M3U8格式视频下载

现在看来,这是之前做过的最有意思的一个功能。😁

M3U8是一种流媒体,现多用于视频播放。当把m3u8格式视频对应的url下载下来用文本打开会发现其实只是一段字符串,包含了很多视频小片段的地址,那么怎么样把整个视频下载下来用于本地播放呢?

m3u8文件

首先,m3u8文件展开后的字符串类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#EXTM3U
#EXT-X-TARGETDURATION:30
#EXT-X-VERSION:2
#EXT-X-DISCONTINUITY
#EXTINF:10,
http://f.youku.com/player/getMpegtsPath/st/flv/fileid/03000201004F4BC6AFD0C202E26EEEB41666A0-C93C-D6C9-9FFA-33424A776707/ipad0_0.ts?KM=14eb49fe4969126c6&start=0&end=10&ts=10&html5=1&seg_no=0&seg_time=0
#EXTINF:20,
http://f.youku.com/player/getMpegtsPath/st/flv/fileid/03000201004F4BC6AFD0C202E26EEEB41666A0-C93C-D6C9-9FFA-33424A776707/ipad0_1.ts?KM=14eb49fe4969126c6&start=10&end=30&ts=20&html5=1&seg_no=1&seg_time=0
#EXTINF:20,
http://f.youku.com/player/getMpegtsPath/st/flv/fileid/03000201004F4BC6AFD0C202E26EEEB41666A0-C93C-D6C9-9FFA-33424A776707/ipad0_2.ts?KM=14eb49fe4969126c6&start=30&end=50&ts=20&html5=1&seg_no=2&seg_time=0
#EXTINF:20,
http://f.youku.com/player/getMpegtsPath/st/flv/fileid/03000201004F4BC6AFD0C202E26EEEB41666A0-C93C-D6C9-9FFA-33424A776707/ipad0_3.ts?KM=14eb49fe4969126c6&start=50&end=70&ts=20&html5=1&seg_no=3&seg_time=0
#EXTINF:24,
http://f.youku.com/player/getMpegtsPath/st/flv/fileid/03000201004F4BC6AFD0C202E26EEEB41666A0-C93C-D6C9-9FFA-33424A776707/ipad0_4.ts?KM=14eb49fe4969126c6&start=70&end=98&ts=24&html5=1&seg_no=4&seg_time=0
#EXT-X-ENDLIST

在每一行#EXTINF:下面的网址就是一个个ts文件的地址,实际去下载的应该是这些视频小片段。而在#EXTINF:后面的数字代表的是这个ts文件所包含视频的时长。

解析

第一步要解析这个m3u8的url,将其中所有ts文件的下载地址记录下来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
NSString *data = [NSString stringWithContentsOfURL:url usedEncoding:&encoding error:&error];
NSMutableArray *segments = [NSMutableArray array];
NSRange segmentRange = [remainData rangeOfString:@"#EXTINF:"];
NSInteger segmentIndex = 0;
NSInteger totalSeconds = 0;
while (segmentRange.location != NSNotFound) {
SCM3U8SegmentInfo *segment = [[SCM3U8SegmentInfo alloc] init];
//读取片段时长
NSRange commaRange = [remainData rangeOfString:@","];
NSString *value = [remainData substringWithRange:NSMakeRange(segmentRange.location + [@"#EXTINF:" length], commaRange.location -(segmentRange.location + [@"#EXTINF:" length]))];
segment.duration = [value intValue];
totalSeconds+=segment.duration;
remainData = [remainData substringFromIndex:commaRange.location];
//读取片段url
NSRange linkRangeBegin = [remainData rangeOfString:@"http"];
NSRange linkRangeEnd = [remainData rangeOfString:@"#"];
NSString *linkurl = [remainData substringWithRange:NSMakeRange(linkRangeBegin.location, linkRangeEnd.location - linkRangeBegin.location)];
segment.url = linkurl;
segment.index = segmentIndex;
//
segmentIndex++;
[segments addObject:segment];
remainData = [remainData substringFromIndex:linkRangeEnd.location];
segmentRange = [remainData rangeOfString:@"#EXTINF:"];
}

下载

得到了这些视频文件的真实地址后,就可以去下载了。下载方法有很多,可以用第三方库ASIHTTPRequest,AFNetworking;或者直接用原生的NSURLConnection,NSURLSession都可以。注意因为一个视频包含很多个小的ts文件,所以为了便于管理,建议1 是将这些文件按索引号分别命名为id0.ts,id1.ts,id2.ts等,2 是按顺序去下载这些小片段,且同一时间只下载一个,当一个下载完成后,再去下载下一个。注意这里其实每一个ts小片段本身都是不支持断点续传的😳。

拼接

当所有这些ts文件下载完成后,需要在本地将它们拼接成一个新的m3u8文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
NSString *pathPrefix = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0];
NSString *saveTo = [[pathPrefix stringByAppendingPathComponent:kPathDownload] stringByAppendingPathComponent:self.vid];
NSString *fullPath = [saveTo stringByAppendingPathComponent:@"movie.m3u8"];
//创建文件头部
NSString* head = @"#EXTM3U\n#EXT-X-TARGETDURATION:30\n#EXT-X-VERSION:2\n#EXT-X-DISCONTINUITY\n";
NSString* segmentPrefix = [NSString stringWithFormat:@"http://127.0.0.1:54321/%@/",self.vid];
NSInteger count = [self.segmentList.segments count];
//填充片段数据
for(int i = 0;i<count;i++){
NSString *filename = [NSString stringWithFormat:@"id%d.ts",i];
SCM3U8SegmentInfo *segInfo = [self.segmentList getSegmentWithIndex:i];
NSString *length = [NSString stringWithFormat:@"#EXTINF:%ld,\n",(long)segInfo.duration];
NSString *url = [segmentPrefix stringByAppendingString:filename];
head = [NSString stringWithFormat:@"%@%@%@\n",head,length,url];
}
//创建尾部
NSString* end = @"#EXT-X-ENDLIST";
head = [head stringByAppendingString:end];
NSMutableData *writer = [[NSMutableData alloc] init];
[writer appendData:[head dataUsingEncoding:NSUTF8StringEncoding]];
[writer writeToFile:fullPath atomically:YES];

其中127.0.0.1是随便写的,只要和播放时调用的一致即可。拼成后本地的m3u8文件用文本打开类似如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#EXTM3U
#EXT-X-TARGETDURATION:30
#EXT-X-VERSION:2
#EXT-X-DISCONTINUITY
#EXTINF:6,
http://127.0.0.1:54321/TTT/id0.ts
#EXTINF:6,
http://127.0.0.1:54321/TTT/id1.ts
#EXTINF:6,
http://127.0.0.1:54321/TTT/id2.ts
#EXTINF:6,
http://127.0.0.1:54321/TTT/id3.ts
#EXTINF:6,
http://127.0.0.1:54321/TTT/id4.ts
#EXTINF:6,
http://127.0.0.1:54321/TTT/id5.ts
#EXTINF:6,
http://127.0.0.1:54321/TTT/id6.ts
#EXTINF:6,
http://127.0.0.1:54321/TTT/id7.ts
#EXTINF:6,
http://127.0.0.1:54321/TTT/id8.ts
#EXT-X-ENDLIST

本地播放

由于m3u8只支持http协议的远程播放,所以必须在应用程序里搭建一个服务器。好消息是早就有开源库CocoaHTTPServer做到了这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)setHttpServer {
_httpServer = [[HTTPServer alloc] init];
[_httpServer setType:@"_http._tcp."];
[_httpServer setPort:54321];
NSString *pathPrefix = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0];
NSString *webPath = [pathPrefix stringByAppendingPathComponent:kPathDownload];
NSLog(@"Setting document root: %@", webPath);
[_httpServer setDocumentRoot:webPath];
NSError *error;
if(![_httpServer start:&error]) {
NSLog(@"Error starting HTTP Server: %@", error);
}
}

最后,将类似127.0.0.1:54321/%@/movie.m3u8这样的地址传给系统播放器就可以实现本地播放了。

完整版Demo请猛戳这里