前言: 之前有專案要實現m3u8本地播放,後面自己寫了一個多執行緒下載的demo,裡面坑很多。因為時間問題,也沒時間去填坑,後面還是採用單執行緒下載。但想了下m3u8本地快取播放這個需求,每個公司的需求可能有所差異。但m3u8是協議好的東西,解析跟組裝的部分應該都差不多。所以下面就記錄下這部分的內容。
-
定義模型儲存資訊
@class ZBLM3u8TsInfo; @interface ZBLM3u8Info : NSObject @property (nonatomic, copy) NSString *targetduration; @property (nonatomic, copy) NSString *version; @property (nonatomic, copy) NSString *mediaSequence; ///key資訊 @property (nonatomic, copy) NSString *keyMethod; @property (nonatomic, copy) NSString *keyUri; @property (nonatomic, copy) NSString *keyLocalUri; @property (nonatomic, copy) NSString *keyIv; @property (nonatomic, strong) NSMutableArray <ZBLM3u8TsInfo*> *tsInfos; @end @interface ZBLM3u8TsInfo : NSObject @property (nonatomic, copy) NSString *duration; @property (nonatomic, copy) NSString *oriUrlString; @property (nonatomic, copy) NSString *localUrlString; @property (nonatomic, assign) NSInteger index; @property (nonatomic, assign,getter = isHasDiscontiunity) BOOL hasDiscontiunity; @end 複製程式碼
-
定義處理類,根據m3u8文字資訊把資訊儲存到模型中並做相關的容錯處理
.h定義
@class ZBLM3u8Info; typedef void (^ZBLM3u8AnalysiseCompletaionHandler)(ZBLM3u8Info *m3u8Info,NSError *error); @interface ZBLM3u8Analysiser : NSObject /** m3u8解析 @param urlStr m3u8網路url @param completaionHandler 回撥 */ + (void)analysisWithUrlString:(NSString*)urlStr completaionHandler:(ZBLM3u8AnalysiseCompletaionHandler)completaionHandler; /** m3u8文字組裝 @param m3u8Info m3u8資訊模型 @return m3u8文字 */ + (NSString*)synthesisLocalM3u8Withm3u8Info:(ZBLM3u8Info *)m3u8Info; FOUNDATION_EXPORT NSString * const ZBLM3u8AnalysiserResponeErrorDomain; FOUNDATION_EXPORT NSString * const ZBLM3u8AnalysiserAnalysisErrorDomain; @end 複製程式碼
.m實現
/*解析m3u8 和組裝m3u8*/ NSString * const ZBLM3u8AnalysiserResponeErrorDomain = @"error.m3u8.analysiser.respone"; NSString * const ZBLM3u8AnalysiserAnalysisErrorDomain = @"error.m3u8.analysiser.analysis"; @implementation ZBLM3u8Analysiser + (void)analysisWithUrlString:(NSString*)urlStr completaionHandler:(ZBLM3u8AnalysiseCompletaionHandler)completaionHandler { NSLog(@"analysis start"); //判斷m3u8文字是否已經儲存到本地,因為下載m3u8文字是耗時操作 NSString *oriM3u8String = [NSString stringWithContentsOfFile:[[ZBLM3u8Setting fullCommonDirPrefixWithUrl:urlStr] stringByAppendingPathComponent:[ZBLM3u8Setting oriM3u8InfoFileName]] encoding:0 error:nil]; __block BOOL happenException = NO; if (oriM3u8String.length) { NSLog(@"use local oriM3u8Info"); //處理有可能失敗,所有需要加try-catch-finally,保護程式 @try { [self analysisWithM3u8String:oriM3u8String completaionHandler:completaionHandler]; } @catch (NSException *exception) { happenException = YES; [[ZBLM3u8FileManager shareInstance]removeFileWithPath:[[ZBLM3u8Setting fullCommonDirPrefixWithUrl:urlStr] stringByAppendingPathComponent:[ZBLM3u8Setting oriM3u8InfoFileName]]]; completaionHandler(nil,[[NSError alloc]initWithDomain:ZBLM3u8AnalysiserAnalysisErrorDomain code:NSURLErrorUnknown userInfo:@{@"info":exception.reason}]); } @finally { } if (!happenException) { return; } } happenException = NO; //非同步獲取 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSString *m3u8Str = nil; //處理有可能失敗,所有需要加try-catch-finally,保護程式 @try { NSError *error = nil; //根據url請求獲取m3u8文字,耗時操作 m3u8Str = [[NSString alloc] initWithContentsOfURL:[NSURL URLWithString:urlStr] usedEncoding:0 error:&error]; if (error) { completaionHandler(nil,error); return ; } if (m3u8Str.length == 0) { completaionHandler(nil,[[NSError alloc]initWithDomain:ZBLM3u8AnalysiserResponeErrorDomain code:NSURLErrorBadServerResponse userInfo:nil]); return; } //解析 [self analysisWithM3u8String:m3u8Str completaionHandler:completaionHandler]; } @catch (NSException *exception) { happenException = YES; completaionHandler(nil,[[NSError alloc]initWithDomain:ZBLM3u8AnalysiserAnalysisErrorDomain code:NSURLErrorUnknown userInfo:@{@"info":exception.reason}]); } @finally { } if (!happenException) { //儲存原m3u8文字,方便二次利用 [[ZBLM3u8FileManager shareInstance]saveDate:[m3u8Str dataUsingEncoding:NSUTF8StringEncoding] ToFile:[[ZBLM3u8Setting fullCommonDirPrefixWithUrl:urlStr] stringByAppendingPathComponent:[ZBLM3u8Setting oriM3u8InfoFileName]] completaionHandler:nil]; } }); } /** private method m3u8文字解析 @param m3u8String m3u8文字 @param completaionHandler 回撥 */ + (void)analysisWithM3u8String:(NSString*)m3u8String completaionHandler:(ZBLM3u8AnalysiseCompletaionHandler)completaionHandler { ZBLM3u8Info *info = [ZBLM3u8Info new]; info.version = [[m3u8String subStringFrom:@"#EXT-X-VERSION:" to:@"#"] removeSpaceAndNewline]; info.targetduration = [[m3u8String subStringFrom:@"#EXT-X-TARGETDURATION:" to:@"#"] removeSpaceAndNewline]; info.mediaSequence = [[m3u8String subStringFrom:@"#EXT-X-MEDIA-SEQUENCE:" to:@"#"] removeSpaceAndNewline]; info.keyMethod = [m3u8String subStringFrom:@"#EXT-X-KEY:METHOD=" to:@","]; info.keyUri = [m3u8String subStringFrom:@"URI=\"" to:@"\""]; info.keyIv = [[m3u8String subStringFrom:@"IV=" to:@"#"] removeSpaceAndNewline]; NSRange tsRange = [m3u8String rangeOfString:@"#EXTINF:"]; if (tsRange.location == NSNotFound) { completaionHandler(nil,[[NSError alloc]initWithDomain:ZBLM3u8AnalysiserAnalysisErrorDomain code:NSURLErrorUnknown userInfo:@{@"info":@"none downloadUrl for .ts file"}]); return; } NSInteger index = 0; NSMutableArray *tsInfos = @[].mutableCopy; m3u8String = [m3u8String substringFromIndex:tsRange.location]; while (tsRange.location != NSNotFound) { ZBLM3u8TsInfo *tsInfo = [ZBLM3u8TsInfo new]; tsInfo.duration = [m3u8String subStringFrom:@"#EXTINF:" to:@","]; m3u8String = [m3u8String subStringForm:@"," offset:1]; tsInfo.oriUrlString = [[m3u8String subStringTo:@"#"] removeSpaceAndNewline]; NSRange exRange = [m3u8String rangeOfString:@"#EX"]; NSRange discontinuityRange = [m3u8String rangeOfString:@"#EXT-X-DISCONTINUITY"]; if (exRange.location == discontinuityRange.location) { tsInfo.hasDiscontiunity = YES; } tsInfo.index = index ++; [tsInfos addObject:tsInfo]; tsRange = [m3u8String rangeOfString:@"#EXTINF:"]; if (tsRange.location != NSNotFound) { m3u8String = [m3u8String subStringForm:@"#EXTINF:" offset:0]; } } NSLog(@"analysis compelte"); info.tsInfos = tsInfos; NSParameterAssert(completaionHandler); completaionHandler(info,nil); } 複製程式碼
-
根據本地儲存的ts資訊更新模型,並重新組裝m3u8文字資訊
+ (NSString*)synthesisLocalM3u8Withm3u8Info:(ZBLM3u8Info *)m3u8Info { NSString *newM3u8String = @""; NSString *header = @"#EXTM3U\n"; if (m3u8Info.version.length) { header = [header stringByAppendingString:[NSString stringWithFormat:@"#EXT-X-VERSION:%ld\n",(long)m3u8Info.version.integerValue]]; } if (m3u8Info.targetduration.length) { header = [header stringByAppendingString:[NSString stringWithFormat:@"#EXT-X-TARGETDURATION:%ld\n",(long)m3u8Info.targetduration.integerValue]]; } if (m3u8Info.mediaSequence.length) { header = [header stringByAppendingString:[NSString stringWithFormat:@"#EXT-X-MEDIA-SEQUENCE:%ld\n",(long)m3u8Info.mediaSequence.integerValue]]; } NSString *keyStr = @""; if(m3u8Info.keyUri.length) { keyStr = [NSString stringWithFormat:@"#EXT-X-KEY:METHOD=%@,URI=\"%@\",IV=%@\n",m3u8Info.keyMethod,m3u8Info.keyLocalUri,m3u8Info.keyIv]; } __block NSString *body = @""; [m3u8Info.tsInfos enumerateObjectsUsingBlock:^(ZBLM3u8TsInfo * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSString *tsInfo = [NSString stringWithFormat:@"#EXTINF:%.6lf,\n%@\n",obj.duration.floatValue,obj.localUrlString]; body = [body stringByAppendingString:tsInfo]; if (obj.isHasDiscontiunity) body = [body stringByAppendingString:@"#EXT-X-DISCONTINUITY\n"]; }]; newM3u8String = [[[[newM3u8String stringByAppendingString:header] stringByAppendingString:keyStr] stringByAppendingString:body] stringByAppendingString:@"#EXT-X-ENDLIST"]; return newM3u8String; } 複製程式碼
demo: github.com/zmubai/ZBLM…