java下载HLS视频

痴心易碎 提交于 2021-01-23 20:07:37

转自大佬https://www.cnblogs.com/developer-ios/p/12460006.html

https://www.jianshu.com/p/dbac4c041de8

 

HLS简介

M3U8,用 UTF-8 编码。"M3U" 和 "M3U8" 文件都是苹果公司使用的 HTTP Live Streaming(HLS) 协议格式的基础; 是 Unicode 版本的 M3U

M3U8文件是M3U文件的一种,只不过它的编码格式是UTF-8。M3U使用Latin-1字符集编码。M3U的全称是Moving Picture Experts Group Audio Layer 3 Uniform Resource Locator,即mp3 URL。M3U是纯文本文件;

所以UTF-8编码的M3U文件也简称为 M3U8;

HLS 是一个由苹果公司提出的基于 HTTP 的流媒体网络传输协议。

HLS 的工作原理是把整个流分成一个个小的基于 HTTP 的文件来下载,每次只下载一些。当媒体流正在播放时,客户端可以选择从许多不同的备用源中以不同的速率下载同样的资源,允许流媒体会话适应不同的数据速率。在开始一个流媒体会话时,客户端会下载一个包含元数据的 extended M3U (m3u8) playlist文件,用于寻找可用的媒体流。

HLS 只请求基本的 HTTP 报文,与实时传输协议(RTP)不同,HLS 可以穿过任何允许 HTTP 数据通过的防火墙或者代理服务器。它也很容易使用内容分发网络来传输媒体流。这是HLS应用在直播上的一大优势。

如果在直播中使用HLS技术,那么执行流程如下:图片来源于苹果官网;

 
 
 

我们播放一个HLS,首先要对HLS流对应的M3U8文件进行解析,解析M3U8文件,首先要搞清楚M3U8的封装格式;

HLS格式解析

HLS流可以用于直播,也可以用于点播;

M3U8 文件实质是一个播放列表(playlist),其可能是一个媒体播放列表(Media Playlist),或者是一个主列表(Master Playlist)。

1.HLS类型

当 M3U8 文件作为媒体播放列表(Media Playlist)时,其内部信息记录的是一系列媒体片段资源,顺序播放该片段资源,即可完整展示多媒体资源。其格式如下所示:

#EXTM3U
#EXT-X-TARGETDURATION:10

#EXTINF:9.009,
http://media.example.com/first.ts
#EXTINF:9.009,
http://media.example.com/second.ts
#EXTINF:3.003,
http://media.example.com/third.ts
#EXT-X-ENDLIST

当 M3U8 作为主播放列表(Master Playlist)时,其内部提供的是同一份媒体资源的多份流列表资源。其格式如下所示:

#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/low/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/lo_mid/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/hi_mid/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/high/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5"
http://example.com/audio/index.m3u8
#EXT-X-ENDLIST

2.HLS基本字段


#EXTM3U                    M3U8文件头,必须放在第一行;
#EXT-X-MEDIA-SEQUENCE      第一个TS分片的序列号,一般情况下是0,但是在直播场景下,这个序列号标识直播段的起始位置; #EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION      每个分片TS的最大的时长;   #EXT-X-TARGETDURATION:10     每个分片的最大时长是 10s
#EXT-X-ALLOW-CACHE         是否允许cache;          #EXT-X-ALLOW-CACHE:YES      #EXT-X-ALLOW-CACHE:NO    默认情况下是YES
#EXT-X-ENDLIST             M3U8文件结束符;
#EXTINF                    extra info,分片TS的信息,如时长,带宽等;一般情况下是    #EXTINF:<duration>,[<title>] 后面可以跟着其他的信息,逗号之前是当前分片的ts时长,分片时长 移动要小于 #EXT-X-TARGETDURATION 定义的值;
#EXT-X-VERSION             M3U8版本号
#EXT-X-DISCONTINUITY       该标签表明其前一个切片与下一个切片之间存在中断。下面会详解
#EXT-X-PLAYLIST-TYPE       表明流媒体类型;
#EXT-X-KEY                 是否加密解析,    #EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"    加密方式是AES-128,秘钥需要请求   https://priv.example.com/key.php?r=52  ,请求回来存储在本地;

3.如何判断HLS是否是直播

  • 1.判断是否存在 #EXT-X-ENDLIST

对于一个M3U8文件,如果结尾不存在 #EXT-X-ENDLIST,那么一定是 直播,不是点播;

  • 2.判断 #EXT-X-PLAYLIST-TYPE 类型

'#EXT-X-PLAYLIST-TYPE' 有两种类型,

VOD 即 Video on Demand,表示该视频流为点播源,因此服务器不能更改该 M3U8 文件;

EVENT 表示该视频流为直播源,因此服务器不能更改或删除该文件任意部分内容(但是可以在文件末尾添加新内容)(注:VOD 文件通常带有 EXT-X-ENDLIST 标签,因为其为点播源,不会改变;而 EVEVT 文件初始化时一般不会有 EXT-X-ENDLIST 标签,暗示有新的文件会添加到播放列表末尾,因此也需要客户端定时获取该 M3U8 文件,以获取新的媒体片段资源,直到访问到 EXT-X-ENDLIST 标签才停止)。

4.HLS提供多码率

上面的Master Playlist 就是会提供 多份码率的列表资源,如下:


#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/low/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/lo_mid/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/hi_mid/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/high/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5"
http://example.com/audio/index.m3u8
#EXT-X-ENDLIST

'#EXT-X-STREAM-INF' 字段后面有:
BANDWIDTH 指定码率
RESOLUTION 分辨率
PROGRAM-ID 唯一ID
CODECS 指定流的编码类型

码率、码流是同一个概念,是数据传输时单位时间传送的数据量,一般用单位kbps表示。

视频码率就是指视频文件在单位时间内使用的数据量。简单理解就是要播放一秒的视频需要多少数据,从这个角度就不难理解通常码率越高视频质量也越好,相应的文件体积也会越大。码率、视频质量、文件体积是正相关的。但当码率超过一定数值后,对图像的质量影响就不大了。几乎所有的编码算法都在追求用最低的码率达到最少的失真(最好的清晰度);

5.HLS中插入广告

M3U8文件中插入广告,要想灵活的控制广告,则广告可以插入任何视频中,那么无法保证广告的编码格式和码率等信息和原视频的编码格式等信息保持一致,就必须告知播放器,在插入广告的地方,ts片段发生的信息变更,需要播放器适配处理。

'#EXT-X-DISCONTINUITY' 该标签表明其前一个切片与下一个切片之间存在中断。说明有不连续的视频出现,这个视频绝大多数情况下就是广告;
'#EXT-X-DISCONTINUITY' 这个字段就是来做这个事情的;
当出现以下情况时,必须使用该标签:

  • file format
  • encoding parameters

下面展示一个插入广告的例子:

#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
movieA.ts
#EXTINF:10.0,
movieB.ts
 ...
#EXT-X-ENDLIST

想在开头插入广告:

#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
ad0.ts
#EXTINF:8.0,
ad1.ts
#EXT-X-DISCONTINUITY
#EXTINF:10.0,
movieA.ts
#EXTINF:10.0,
movieB.ts
...
#EXT-X-ENDLIST

当然你可以在任意位置插入广告。

HLS协议草案:HLS协议中还有很多字段,但是有些字段其实就是协议,在实际应用中并不大;大家可以参考看看;

https://tools.ietf.org/html/rfc8216

启示

1.视频切换清晰度功能

目前线上有很多的M3U8文件都提供多码率(多种清晰度)的选择,我们可以改进我们的功能,为用户提供多种视频清晰度的选择;

2.视频广告

目前M3U8视频占我们线上视频的比例是近60%,量非常大,我们可以在M3U8视频中任意位置插入一些广告,为探索商业化开辟新的路。

 ★★★★★★★★★★★★★★★★★★★★★★★★★★下面是下载方法★★★★★★★★★★★★★★★★★★★★★★★★★★★★★

下载索引文件

public String getIndexFile() throws Exception{ URL url = new URL(originUrlpath); //下载资源 BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream(),"UTF-8")); String content = "" ; String line; while ((line = in.readLine()) != null) { content += line + "\n"; } in.close(); return content; }

解析索引文件

public List analysisIndex(String content) throws Exception{ Pattern pattern = Pattern.compile(".*ts"); Matcher ma = pattern.matcher(content); List<String> list = new ArrayList<String>(); while(ma.find()){ list.add(ma.group()); } return list; }

下载视频片段

同步下载

public HashMap downLoadIndexFile(List<String> urlList){
    HashMap<Integer,String> keyFileMap = new HashMap<>(); for(int i =0;i<urlList.size();i++){ String subUrlPath = urlList.get(i); String fileOutPath = folderPath + File.separator + i + ".ts"; keyFileMap.put(i, fileOutPath); try{ downloadNet(preUrlPath + subUrlPath, fileOutPath); System.out.println("成功:"+ (i + 1) +"/" + urlList.size()); }catch (Exception e){ System.err.println("失败:"+ (i + 1) +"/" + urlList.size()); } } return keyFileMap; } private void downloadNet(String fullUrlPath, String fileOutPath) throws Exception { //int bytesum = 0; int byteread = 0; URL url = new URL(fullUrlPath); URLConnection conn = url.openConnection(); InputStream inStream = conn.getInputStream(); FileOutputStream fs = new FileOutputStream(fileOutPath); byte[] buffer = new byte[1204]; while ((byteread = inStream.read(buffer)) != -1) { //bytesum += byteread; fs.write(buffer, 0, byteread); } }

多线程下载

public void downLoadIndexFileAsync(List<String> urlList, HashMap<Integer,String> keyFileMap) throws Exception{ int downloadForEveryThread = (urlList.size() + threadQuantity - 1)/threadQuantity; if(downloadForEveryThread == 0) downloadForEveryThread = urlList.size(); for(int i=0; i<urlList.size();i+=downloadForEveryThread){ int startIndex = i; int endIndex = i + downloadForEveryThread - 1; if(endIndex >= urlList.size()) endIndex = urlList.size() - 1; new DownloadThread(urlList, startIndex, endIndex, keyFileMap).start(); } } class DownloadThread extends Thread{ private List<String> urlList; private int startIndex; private int endIndex; private HashMap<Integer,String> keyFileMap; public DownloadThread(List<String> urlList, int startIndex, int endIndex, HashMap<Integer,String> keyFileMap){ this.urlList = urlList; this.startIndex = startIndex; this.endIndex = endIndex; this.keyFileMap = keyFileMap; } @Override public void run(){ for(int i=startIndex;i<=endIndex;i++){ String subUrlPath = urlList.get(i); String fileOutPath = folderPath + File.separator + i + ".ts"; keyFileMap.put(i, fileOutPath); String message = "%s: 线程 " + (startIndex/(endIndex - startIndex) + 1) + ", "+ (i + 1) +"/" + urlList.size() +", 合计: %d"; try{ downloadNet(preUrlPath + subUrlPath, fileOutPath); System.out.println(String.format(message, "成功", keyFileMap.size())); }catch (Exception e){ System.err.println(String.format(message, "失败", keyFileMap.size())); } } } }

视频片段合成

public String composeFile(HashMap<Integer,String> keyFileMap) throws Exception{ if(keyFileMap.isEmpty()) return null; String fileOutPath = rootPath + File.separator + fileName; FileOutputStream fileOutputStream = new FileOutputStream(new File(fileOutPath)); byte[] bytes = new byte[1024]; int length = 0; for(int i=0; i<keyFileMap.size(); i++){ String nodePath = keyFileMap.get(i); File file = new File(nodePath); if(!file.exists()) continue; FileInputStream fis = new FileInputStream(file); while ((length = fis.read(bytes)) != -1) { fileOutputStream.write(bytes, 0, length); } } return fileName; }
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!