diff --git a/ios/.swiftformat b/ios/.swiftformat index f18154f318..f9abda7449 100644 --- a/ios/.swiftformat +++ b/ios/.swiftformat @@ -10,7 +10,5 @@ --enable markTypes ---enable isEmpty - --funcattributes "prev-line" --maxwidth 160 \ No newline at end of file diff --git a/ios/RCTVideo.xcodeproj/project.pbxproj b/ios/RCTVideo.xcodeproj/project.pbxproj index 1665197d03..6f5365ead8 100644 --- a/ios/RCTVideo.xcodeproj/project.pbxproj +++ b/ios/RCTVideo.xcodeproj/project.pbxproj @@ -49,6 +49,11 @@ 0177D39827170A7A00F5BE18 /* RCTVideo-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "RCTVideo-Bridging-Header.h"; path = "Video/RCTVideo-Bridging-Header.h"; sourceTree = ""; }; 0177D39927170A7A00F5BE18 /* RCTVideo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RCTVideo.swift; path = Video/RCTVideo.swift; sourceTree = ""; }; 134814201AA4EA6300B7C361 /* libRCTVideo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTVideo.a; sourceTree = BUILT_PRODUCTS_DIR; }; + DCF8FEFD2C1A174E00CCA6B0 /* CurrentVideos.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = CurrentVideos.h; path = Video/CurrentVideos.h; sourceTree = ""; }; + DCF8FEFE2C1A174E00CCA6B0 /* CurrentVideos.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = CurrentVideos.m; path = Video/CurrentVideos.m; sourceTree = ""; }; + DCF8FEFF2C1A18AC00CCA6B0 /* WeakVideoRef.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = WeakVideoRef.h; path = Video/WeakVideoRef.h; sourceTree = ""; }; + DCF8FF002C1A18AC00CCA6B0 /* WeakVideoRef.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = WeakVideoRef.m; path = Video/WeakVideoRef.m; sourceTree = ""; }; + DCF8FF322C1AF64800CCA6B0 /* M3U8Kit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = M3U8Kit; path = Video/M3U8Kit; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -87,10 +92,15 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( + DCF8FF322C1AF64800CCA6B0 /* M3U8Kit */, 01489050272001A100E69940 /* DataStructures */, 01489051272001A100E69940 /* Features */, 0177D39527170A7A00F5BE18 /* RCTSwiftLog */, 0177D39927170A7A00F5BE18 /* RCTVideo.swift */, + DCF8FEFD2C1A174E00CCA6B0 /* CurrentVideos.h */, + DCF8FEFE2C1A174E00CCA6B0 /* CurrentVideos.m */, + DCF8FEFF2C1A18AC00CCA6B0 /* WeakVideoRef.h */, + DCF8FF002C1A18AC00CCA6B0 /* WeakVideoRef.m */, 0177D39727170A7A00F5BE18 /* RCTVideoManager.m */, 0177D39227170A7A00F5BE18 /* RCTVideoManager.swift */, 0177D39427170A7A00F5BE18 /* RCTVideoPlayerViewController.swift */, diff --git a/ios/Video/CurrentVideos.h b/ios/Video/CurrentVideos.h new file mode 100644 index 0000000000..887ee158e9 --- /dev/null +++ b/ios/Video/CurrentVideos.h @@ -0,0 +1,19 @@ +// +// CurrentVideos.h +// react-native-video +// +// Created by marcin.dziennik on 9/8/23. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class RCTVideo; +@interface CurrentVideos : NSObject ++ (instancetype)shared; +- (void)add:(RCTVideo*)video forTag:(NSNumber*)tag; +- (nullable RCTVideo*)videoForTag:(NSNumber*)tag; +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/Video/CurrentVideos.m b/ios/Video/CurrentVideos.m new file mode 100644 index 0000000000..6f9d7856ed --- /dev/null +++ b/ios/Video/CurrentVideos.m @@ -0,0 +1,54 @@ +// +// CurrentVideos.m +// react-native-video +// +// Created by marcin.dziennik on 9/8/23. +// + +#import "CurrentVideos.h" +#import "WeakVideoRef.h" + +@interface CurrentVideos () +@property(strong) NSMutableDictionary* videos; +@end + +@implementation CurrentVideos + ++ (nonnull instancetype)shared { + static CurrentVideos* instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[self alloc] init]; + }); + return instance; +} + +- (instancetype)init { + if (self = [super init]) { + _videos = [[NSMutableDictionary alloc] init]; + } + + return self; +} + +- (void)add:(nonnull RCTVideo*)video forTag:(nonnull NSNumber*)tag { + [self cleanup]; + WeakVideoRef* ref = [[WeakVideoRef alloc] init]; + ref.video = video; + [_videos setObject:ref forKey:tag]; +} + +- (nullable RCTVideo*)videoForTag:(nonnull NSNumber*)tag { + [self cleanup]; + return [[_videos objectForKey:tag] video]; +} + +- (void)cleanup { + for (NSNumber* key in [_videos allKeys]) { + if (_videos[key].video == nil) { + [_videos removeObjectForKey:key]; + } + } +} + +@end diff --git a/ios/Video/Features/RCTPlayerOperations.swift b/ios/Video/Features/RCTPlayerOperations.swift index 9c80c85f3e..c3a0bf43d8 100644 --- a/ios/Video/Features/RCTPlayerOperations.swift +++ b/ios/Video/Features/RCTPlayerOperations.swift @@ -1,5 +1,6 @@ import AVFoundation import MediaAccessibility +import MediaPlayer let RCTVideoUnset = -1 @@ -9,6 +10,8 @@ let RCTVideoUnset = -1 * Collection of mutating functions */ enum RCTPlayerOperations { + static var remoteCommandHandlerForSpatialAudio: Any? + static func setSideloadedText(player: AVPlayer?, textTracks: [TextTrack], criteria: SelectedTrackCriteria?) { let type = criteria?.type @@ -116,12 +119,20 @@ enum RCTPlayerOperations { } } } else { // default. invalid type or "system" - await player?.currentItem?.selectMediaOptionAutomatically(in: group) + #if os(tvOS) + // Do noting. Fix for tvOS native audio menu language selector + #else + await player?.currentItem?.selectMediaOptionAutomatically(in: group) + #endif return } - // If a match isn't found, option will be nil and text tracks will be disabled - await player?.currentItem?.select(mediaOption, in: group) + #if os(tvOS) + // Do noting. Fix for tvOS native audio menu language selector + #else + // If a match isn't found, option will be nil and text tracks will be disabled + await player?.currentItem?.select(mediaOption, in: group) + #endif } static func seek(player: AVPlayer, playerItem: AVPlayerItem, paused: Bool, seekTime: Float, seekTolerance: Float, completion: @escaping (Bool) -> Void) { @@ -196,4 +207,27 @@ enum RCTPlayerOperations { } } } + + // MARK: - Spatial Audio / Dolby Atmos Workaround + + /* These functions are a temporarily workaround to enable the rendering of Dolby Atmos on + * iOS 15 and above. + */ + + static func addSpatialAudioRemoteCommandHandler() { + let command = MPRemoteCommandCenter.shared().playCommand + + remoteCommandHandlerForSpatialAudio = command.addTarget(handler: { _ in + MPRemoteCommandHandlerStatus.success + }) + } + + static func removeSpatialAudioRemoteCommandHandler() { + let command = MPRemoteCommandCenter.shared().playCommand + + if let remoteCommand = RCTPlayerOperations.remoteCommandHandlerForSpatialAudio { + command.removeTarget(remoteCommand) + RCTPlayerOperations.remoteCommandHandlerForSpatialAudio = nil + } + } } diff --git a/ios/Video/M3U8Kit/LICENSE b/ios/Video/M3U8Kit/LICENSE new file mode 100644 index 0000000000..580858ea09 --- /dev/null +++ b/ios/Video/M3U8Kit/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Sun Jin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/ios/Video/M3U8Kit/Source/M3U8ExtXByteRange.h b/ios/Video/M3U8Kit/Source/M3U8ExtXByteRange.h new file mode 100644 index 0000000000..dfe451e4f9 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8ExtXByteRange.h @@ -0,0 +1,44 @@ +// +// M3U8ExtXByteRange.h +// M3U8Kit +// +// Created by Frank on 2020/10/1. +// Copyright © 2020 M3U8Kit. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface M3U8ExtXByteRange : NSObject + +- (instancetype)initWithAtString:(NSString*)atString; +- (instancetype)initWithLength:(NSInteger)length offset:(NSInteger)offset; + +@property(nonatomic, assign, readonly) NSInteger length; +@property(nonatomic, assign, readonly) NSInteger offset; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/Video/M3U8Kit/Source/M3U8ExtXByteRange.m b/ios/Video/M3U8Kit/Source/M3U8ExtXByteRange.m new file mode 100644 index 0000000000..cf28e99122 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8ExtXByteRange.m @@ -0,0 +1,54 @@ +// +// M3U8ExtXByteRange.m +// M3U8Kit +// +// Created by Frank on 2020/10/1. +// Copyright © 2020 M3U8Kit. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8ExtXByteRange.h" + +@implementation M3U8ExtXByteRange + +- (instancetype)initWithAtString:(NSString*)atString { + NSArray* params = [atString componentsSeparatedByString:@"@"]; + NSInteger length = params.firstObject.integerValue; + NSInteger offset = 0; + if (params.count > 1) { + offset = MAX(0, params[1].integerValue); + } + + return [self initWithLength:length offset:offset]; +} + +- (instancetype)initWithLength:(NSInteger)length offset:(NSInteger)offset { + self = [super init]; + if (self) { + _length = length; + _offset = offset; + } + return self; +} + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8ExtXKey.h b/ios/Video/M3U8Kit/Source/M3U8ExtXKey.h new file mode 100644 index 0000000000..1be573e24e --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8ExtXKey.h @@ -0,0 +1,40 @@ +// +// M3U8ExtXKey.h +// M3U8Kit +// +// Created by Pierre Perrin on 01/02/2019. +// Copyright © 2019 M3U8Kit. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import +@interface M3U8ExtXKey : NSObject + +- (instancetype)initWithDictionary:(NSDictionary*)dictionary; + +- (NSString*)method; +- (NSString*)url; +- (NSString*)keyFormat; +- (NSString*)iV; + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8ExtXKey.m b/ios/Video/M3U8Kit/Source/M3U8ExtXKey.m new file mode 100644 index 0000000000..42da2e5979 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8ExtXKey.m @@ -0,0 +1,66 @@ +// +// M3U8ExtXKey.m +// M3U8Kit +// +// Created by Pierre Perrin on 01/02/2019. +// Copyright © 2019 M3U8Kit. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8ExtXKey.h" +#import "M3U8TagsAndAttributes.h" + +@interface M3U8ExtXKey () +@property(nonatomic, strong) NSDictionary* dictionary; +@end + +@implementation M3U8ExtXKey + +- (instancetype)initWithDictionary:(NSDictionary*)dictionary { + if (self = [super init]) { + self.dictionary = dictionary; + } + return self; +} + +- (NSString*)method { + return self.dictionary[M3U8_EXT_X_KEY_METHOD]; +} + +- (NSString*)url { + return self.dictionary[M3U8_EXT_X_KEY_URI]; +} + +- (NSString*)keyFormat { + return self.dictionary[M3U8_EXT_X_KEY_KEYFORMAT]; +} + +- (NSString*)iV { + return self.dictionary[M3U8_EXT_X_KEY_IV]; +} + +- (NSString*)description { + return self.dictionary.description; +} + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8ExtXMedia.h b/ios/Video/M3U8Kit/Source/M3U8ExtXMedia.h new file mode 100644 index 0000000000..80633fe174 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8ExtXMedia.h @@ -0,0 +1,79 @@ +// +// M3U8ExtXMedia.h +// M3U8Kit +// +// Created by Sun Jin on 3/25/14. +// Copyright (c) 2014 Jin Sun. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import + +/* + + /// EXT-X-MEDIA + + @format #EXT-X-MEDIA: , attibute-list: ATTR=,... + @example +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="600k",LANGUAGE="eng",NAME="Audio",AUTOSELECT=YES,DEFAULT=YES,URI="/talks/769/audio/600k.m3u8?sponsor=Ripple",BANDWIDTH=614400 + +#define M3U8_EXT_X_MEDIA @"#EXT-X-MEDIA:" +// EXT-X-MEDIA attributes +#define M3U8_EXT_X_MEDIA_TYPE @"TYPE" // The value is enumerated-string; valid strings are AUDIO, VIDEO, SUBTITLES and +CLOSED-CAPTIONS. #define M3U8_EXT_X_MEDIA_URI @"URI" // The value is a quoted-string containing a URI that identifies the +Playlist file. #define M3U8_EXT_X_MEDIA_GROUP_ID @"GROUP-ID" // The value is a quoted-string identifying a mutually-exclusive +group of renditions. #define M3U8_EXT_X_MEDIA_LANGUAGE @"LANGUAGE" // The value is a quoted-string containing an RFC 5646 +[RFC5646] language tag that identifies the primary language used in the rendition. #define M3U8_EXT_X_MEDIA_ASSOC_LANGUAGE @"ASSOC-LANGUAGE" +// The value is a quoted-string containing an RFC 5646 [RFC5646](http://tools.ietf.org/html/rfc5646) language tag that identifies a language +that is associated with the rendition. #define M3U8_EXT_X_MEDIA_NAME @"NAME" // The value is a quoted-string containing a +human-readable description of the rendition. #define M3U8_EXT_X_MEDIA_DEFAULT @"DEFAULT" // The value is an enumerated-string; +valid strings are YES and NO. #define M3U8_EXT_X_MEDIA_AUTOSELECT @"AUTOSELECT" // The value is an enumerated-string; valid strings +are YES and NO. #define M3U8_EXT_X_MEDIA_FORCED @"FORCED" // The value is an enumerated-string; valid strings are YES and NO. +#define M3U8_EXT_X_MEDIA_INSTREAM_ID @"INSTREAM-ID" // The value is a quoted-string that specifies a rendition within the segments in +the Media Playlist. #define M3U8_EXT_X_MEDIA_CHARACTERISTICS @"CHARACTERISTICS" // The value is a quoted-string containing one or more +Uniform Type Identifiers [UTI] separated by comma (,) characters. + + */ + +@interface M3U8ExtXMedia : NSObject + +- (instancetype)initWithDictionary:(NSDictionary*)dictionary; + +- (NSString*)type; +- (NSURL*)URI; +- (NSString*)groupId; +- (NSString*)channels; +- (NSString*)language; +- (NSString*)assocLanguage; +- (NSString*)name; +- (BOOL)isDefault; +- (BOOL)autoSelect; +- (BOOL)forced; +- (NSString*)instreamId; +- (NSString*)characteristics; +- (NSInteger)bandwidth; + +- (NSURL*)m3u8URL; // the absolute url of media playlist file +- (NSString*)m3u8PlainString; + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8ExtXMedia.m b/ios/Video/M3U8Kit/Source/M3U8ExtXMedia.m new file mode 100644 index 0000000000..79fda34c18 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8ExtXMedia.m @@ -0,0 +1,150 @@ +// +// M3U8ExtXMedia.m +// M3U8Kit +// +// Created by Sun Jin on 3/25/14. +// Copyright (c) 2014 Jin Sun. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8ExtXMedia.h" +#import "M3U8TagsAndAttributes.h" + +@interface M3U8ExtXMedia () +@property(nonatomic, strong) NSDictionary* dictionary; +@end + +@implementation M3U8ExtXMedia + +- (instancetype)initWithDictionary:(NSDictionary*)dictionary { + if (self = [super init]) { + self.dictionary = dictionary; + } + return self; +} + +- (NSURL*)baseURL { + return self.dictionary[M3U8_BASE_URL]; +} + +- (NSURL*)URL { + return self.dictionary[M3U8_URL]; +} + +- (NSString*)type { + return self.dictionary[M3U8_EXT_X_MEDIA_TYPE]; +} + +- (NSURL*)URI { + NSString* originalUrl = self.dictionary[M3U8_EXT_X_MEDIA_URI]; + NSString* urlString = [originalUrl stringByReplacingOccurrencesOfString:@" " withString:@"%20"]; + return [NSURL URLWithString:urlString]; +} + +- (NSString*)groupId { + return self.dictionary[M3U8_EXT_X_MEDIA_GROUP_ID]; +} + +- (NSString*)channels { + return self.dictionary[M3U8_EXT_X_MEDIA_CHANNELS]; +} + +- (NSString*)language { + return [self.dictionary[M3U8_EXT_X_MEDIA_LANGUAGE] lowercaseString]; +} + +- (NSString*)assocLanguage { + return self.dictionary[M3U8_EXT_X_MEDIA_ASSOC_LANGUAGE]; +} + +- (NSString*)name { + return self.dictionary[M3U8_EXT_X_MEDIA_NAME]; +} + +- (BOOL)isDefault { + return [self.dictionary[M3U8_EXT_X_MEDIA_DEFAULT] boolValue]; +} + +- (BOOL)autoSelect { + return [self.dictionary[M3U8_EXT_X_MEDIA_AUTOSELECT] boolValue]; +} + +- (BOOL)forced { + return [self.dictionary[M3U8_EXT_X_MEDIA_FORCED] boolValue]; +} + +- (NSString*)instreamId { + return self.dictionary[M3U8_EXT_X_MEDIA_INSTREAM_ID]; +} + +- (NSString*)characteristics { + return self.dictionary[M3U8_EXT_X_MEDIA_CHARACTERISTICS]; +} + +- (NSInteger)bandwidth { + return [self.dictionary[M3U8_EXT_X_MEDIA_BANDWIDTH] integerValue]; +} + +- (NSURL*)m3u8URL { + if (self.URI.scheme) { + return self.URI; + } + + NSString* originalUrl = self.URI.absoluteString; + NSString* urlString = [originalUrl stringByReplacingOccurrencesOfString:@" " withString:@"%20"]; + return [NSURL URLWithString:urlString relativeToURL:[self baseURL]]; +} + +- (NSString*)description { + return [NSString stringWithString:self.dictionary.description]; +} + +/* + #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="600k",LANGUAGE="eng",NAME="Audio",AUTOSELECT=YES,DEFAULT=YES,URI="main_media_7.m3u8",BANDWIDTH=614400 + */ +- (NSString*)m3u8PlainString { + NSMutableString* str = [NSMutableString string]; + [str appendString:M3U8_EXT_X_MEDIA]; + [str appendString:[NSString stringWithFormat:@"TYPE=%@", self.type]]; + [str appendString:[NSString stringWithFormat:@",GROUP-ID=\"%@\"", self.groupId]]; + [str appendString:[NSString stringWithFormat:@",LANGUAGE=\"%@\"", self.language]]; + [str appendString:[NSString stringWithFormat:@",NAME=\"%@\"", self.name]]; + [str appendString:[NSString stringWithFormat:@",AUTOSELECT=%@", self.autoSelect ? @"YES" : @"NO"]]; + [str appendString:[NSString stringWithFormat:@",DEFAULT=%@", self.isDefault ? @"YES" : @"NO"]]; + + NSString* fStr = self.dictionary[M3U8_EXT_X_MEDIA_FORCED]; + if (fStr.length > 0) { + [str appendString:[NSString stringWithFormat:@",FORCED=%@", fStr]]; + } + + [str appendString:[NSString stringWithFormat:@",URI=\"%@\"", self.URI.absoluteString]]; + + NSString* bStr = self.dictionary[M3U8_EXT_X_MEDIA_BANDWIDTH]; + if (bStr.length > 0) { + [str appendString:[NSString stringWithFormat:@",BANDWIDTH=%@", bStr]]; + } + + return str; +} + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8ExtXMediaList.h b/ios/Video/M3U8Kit/Source/M3U8ExtXMediaList.h new file mode 100644 index 0000000000..0c00f016cb --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8ExtXMediaList.h @@ -0,0 +1,51 @@ +// +// M3U8ExtXMediaList.h +// M3U8Kit +// +// Created by Sun Jin on 3/25/14. +// Copyright (c) 2014 Jin Sun. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8ExtXMedia.h" +#import + +@interface M3U8ExtXMediaList : NSObject + +@property(nonatomic, assign, readonly) NSUInteger count; + +- (void)addExtXMedia:(M3U8ExtXMedia*)extXMedia; +- (M3U8ExtXMedia*)xMediaAtIndex:(NSUInteger)index; +- (M3U8ExtXMedia*)firstExtXMedia; +- (M3U8ExtXMedia*)lastExtXMedia; + +- (M3U8ExtXMediaList*)audioList; +- (M3U8ExtXMedia*)suitableAudio; + +- (M3U8ExtXMediaList*)videoList; +- (M3U8ExtXMedia*)suitableVideo; + +- (M3U8ExtXMediaList*)subtitleList; +- (M3U8ExtXMedia*)suitableSubtitle; + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8ExtXMediaList.m b/ios/Video/M3U8Kit/Source/M3U8ExtXMediaList.m new file mode 100644 index 0000000000..8b9d522754 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8ExtXMediaList.m @@ -0,0 +1,161 @@ +// +// M3U8ExtXMediaList.m +// M3U8Kit +// +// Created by Sun Jin on 3/25/14. +// Copyright (c) 2014 Jin Sun. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8ExtXMediaList.h" + +@interface M3U8ExtXMediaList () + +@property(nonatomic, strong) NSMutableArray* m3u8InfoList; + +@end + +@implementation M3U8ExtXMediaList + +- (id)init { + if (self = [super init]) { + self.m3u8InfoList = [NSMutableArray array]; + } + return self; +} + +- (NSUInteger)count { + return self.m3u8InfoList.count; +} + +- (void)addExtXMedia:(M3U8ExtXMedia*)extXMedia { + if (extXMedia) { + [self.m3u8InfoList addObject:extXMedia]; + } +} + +- (M3U8ExtXMedia*)xMediaAtIndex:(NSUInteger)index { + if (index >= self.count) { + return nil; + } + return [self.m3u8InfoList objectAtIndex:index]; +} + +- (M3U8ExtXMedia*)firstExtXMedia { + return self.m3u8InfoList.firstObject; +} + +- (M3U8ExtXMedia*)lastExtXMedia { + return self.m3u8InfoList.lastObject; +} + +- (M3U8ExtXMediaList*)audioList { + M3U8ExtXMediaList* audioList = [[M3U8ExtXMediaList alloc] init]; + NSArray* copy = [self.m3u8InfoList copy]; + for (M3U8ExtXMedia* media in copy) { + if ([media.type isEqualToString:@"AUDIO"]) { + [audioList addExtXMedia:media]; + } + } + return audioList; +} + +- (M3U8ExtXMedia*)suitableAudio { + NSString* lan = [NSLocale preferredLanguages].firstObject; + NSArray* copy = [self.m3u8InfoList copy]; + M3U8ExtXMedia* suitableAudio = nil; + for (M3U8ExtXMedia* media in copy) { + if ([media.type isEqualToString:@"AUDIO"]) { + if (nil == suitableAudio) { + suitableAudio = media; + } + if ([media.language isEqualToString:lan]) { + suitableAudio = media; + } + } + } + return suitableAudio; +} + +- (M3U8ExtXMediaList*)videoList { + M3U8ExtXMediaList* videoList = [[M3U8ExtXMediaList alloc] init]; + NSArray* copy = [self.m3u8InfoList copy]; + for (M3U8ExtXMedia* media in copy) { + if ([media.type isEqualToString:@"VIDEO"]) { + [videoList addExtXMedia:media]; + } + } + return videoList; +} + +- (M3U8ExtXMedia*)suitableVideo { + NSString* lan = [NSLocale preferredLanguages].firstObject; + NSArray* copy = [self.m3u8InfoList copy]; + M3U8ExtXMedia* suitableVideo = nil; + for (M3U8ExtXMedia* media in copy) { + if ([media.type isEqualToString:@"VIDEO"]) { + + if (nil == suitableVideo) { + suitableVideo = media; + } + if ([media.language isEqualToString:lan]) { + suitableVideo = media; + } + } + } + return suitableVideo; +} + +- (M3U8ExtXMediaList*)subtitleList { + M3U8ExtXMediaList* subtitleList = [[M3U8ExtXMediaList alloc] init]; + NSArray* copy = [self.m3u8InfoList copy]; + for (M3U8ExtXMedia* media in copy) { + if ([media.type isEqualToString:@"SUBTITLES"]) { + [subtitleList addExtXMedia:media]; + } + } + return subtitleList; +} + +- (M3U8ExtXMedia*)suitableSubtitle { + NSString* lan = [NSLocale preferredLanguages].firstObject; + NSArray* copy = [self.m3u8InfoList copy]; + M3U8ExtXMedia* suitableSubtitle = nil; + for (M3U8ExtXMedia* media in copy) { + if ([media.type isEqualToString:@"SUBTITLES"]) { + if (nil == suitableSubtitle) { + suitableSubtitle = media; + } + if ([media.language isEqualToString:lan]) { + suitableSubtitle = media; + } + } + } + return suitableSubtitle; +} + +- (NSString*)description { + return [NSString stringWithString:self.m3u8InfoList.description]; +} + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8ExtXStreamInf.h b/ios/Video/M3U8Kit/Source/M3U8ExtXStreamInf.h new file mode 100644 index 0000000000..4a779c8af2 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8ExtXStreamInf.h @@ -0,0 +1,88 @@ +// +// M3U8ExtXStreamInf.h +// ILSLoader +// +// Created by Jin Sun on 13-4-15. +// Copyright (c) 2013年 iLegendSoft. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import + +struct MediaResoulution { + float width; + float height; +}; +typedef struct MediaResoulution MediaResoulution; + +extern MediaResoulution MediaResolutionMake(float width, float height); + +extern const MediaResoulution MediaResoulutionZero; +NSString* NSStringFromMediaResolution(MediaResoulution resolution); + +/*! + @class M3U8SegmentInfo + @abstract This is the class indicates #EXT-X-STREAM-INF: + in master playlist file. + + /// EXT-X-STREAM-INF + + @format #EXT-X-STREAM-INF: + + @example #EXT-X-STREAM-INF:AUDIO="600k",BANDWIDTH=915685,PROGRAM-ID=1,CODECS="avc1.42c01e,mp4a.40.2",RESOLUTION=640x360,SUBTITLES="subs" + /talks/769/video/600k.m3u8?sponsor=Ripple + + @note The EXT-X-STREAM-INF tag MUST NOT appear in a Media Playlist. + +#define M3U8_EXT_X_STREAM_INF @"#EXT-X-STREAM-INF:" +// EXT-X-STREAM-INF Attributes +#define M3U8_EXT_X_STREAM_INF_BANDWIDTH @"BANDWIDTH" // The value is a decimal-integer of bits per second. +#define M3U8_EXT_X_STREAM_INF_PROGRAM_ID @"PROGRAM-ID" // The value is a decimal-integer that uniquely identifies a particular +presentation within the scope of the Playlist file. #define M3U8_EXT_X_STREAM_INF_CODECS @"CODECS" // The value is a quoted-string +containing a comma-separated list of formats. #define M3U8_EXT_X_STREAM_INF_RESOLUTION @"RESOLUTION" // The value is a decimal-resolution +describing the approximate encoded horizontal and vertical resolution of video within the presentation. #define M3U8_EXT_X_STREAM_INF_AUDIO +@"AUDIO" // The value is a quoted-string. #define M3U8_EXT_X_STREAM_INF_VIDEO @"VIDEO" // The value is a quoted-string. #define +M3U8_EXT_X_STREAM_INF_SUBTITLES @"SUBTITLES" // The value is a quoted-string. #define M3U8_EXT_X_STREAM_INF_CLOSED_CAPTIONS +@"CLOSED-CAPTIONS" // The value can be either a quoted-string or an enumerated-string with the value NONE. #define M3U8_EXT_X_STREAM_INF_URI +@"URI" // The value is a enumerated-string containing a URI that identifies the Playlist file. + + */ +@interface M3U8ExtXStreamInf : NSObject + +@property(nonatomic, readonly, assign) NSInteger bandwidth; +@property(nonatomic, readonly, assign) NSInteger averageBandwidth; +@property(nonatomic, readonly, assign) NSInteger programId; // removed by draft 12 +@property(nonatomic, readonly, copy) NSArray* codecs; +@property(nonatomic, readonly) MediaResoulution resolution; +@property(nonatomic, readonly, copy) NSString* audio; +@property(nonatomic, readonly, copy) NSString* video; +@property(nonatomic, readonly, copy) NSString* subtitles; +@property(nonatomic, readonly, copy) NSString* closedCaptions; +@property(nonatomic, readonly, copy) NSURL* URI; + +- (instancetype)initWithDictionary:(NSDictionary*)dictionary; + +- (NSURL*)m3u8URL; // the absolute url + +- (NSString*)m3u8PlainString; + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8ExtXStreamInf.m b/ios/Video/M3U8Kit/Source/M3U8ExtXStreamInf.m new file mode 100644 index 0000000000..1d976581c7 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8ExtXStreamInf.m @@ -0,0 +1,163 @@ +// +// M3U8ExtXStreamInf.m +// ILSLoader +// +// Created by Jin Sun on 13-4-15. +// Copyright (c) 2013年 iLegendSoft. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8ExtXStreamInf.h" +#import "M3U8TagsAndAttributes.h" + +const MediaResoulution MediaResoulutionZero = {0.f, 0.f}; + +NSString* NSStringFromMediaResolution(MediaResoulution resolution) { + return [NSString stringWithFormat:@"%gx%g", resolution.width, resolution.height]; +} + +MediaResoulution MediaResolutionFromString(NSString* string) { + NSArray* comps = [string componentsSeparatedByString:@"x"]; + if (comps.count == 2) { + float width = [comps[0] floatValue]; + float height = [comps[1] floatValue]; + return MediaResolutionMake(width, height); + } else { + return MediaResoulutionZero; + } +} + +MediaResoulution MediaResolutionMake(float width, float height) { + MediaResoulution resolution = {width, height}; + return resolution; +} + +@interface M3U8ExtXStreamInf () +@property(nonatomic, strong) NSDictionary* dictionary; +@property(nonatomic) MediaResoulution resolution; +@end + +@implementation M3U8ExtXStreamInf + +- (instancetype)initWithDictionary:(NSDictionary*)dictionary { + if (self = [super init]) { + self.dictionary = dictionary; + self.resolution = MediaResoulutionZero; + } + return self; +} + +- (NSURL*)baseURL { + return self.dictionary[M3U8_BASE_URL]; +} + +- (NSURL*)URL { + return self.dictionary[M3U8_URL]; +} + +- (NSURL*)m3u8URL { + if (self.URI.scheme) { + return self.URI; + } + + return [NSURL URLWithString:self.URI.absoluteString relativeToURL:[self baseURL]]; +} + +- (NSInteger)bandwidth { + return [self.dictionary[M3U8_EXT_X_STREAM_INF_BANDWIDTH] integerValue]; +} + +- (NSInteger)averageBandwidth { + return [self.dictionary[M3U8_EXT_X_STREAM_INF_AVERAGE_BANDWIDTH] integerValue]; +} + +- (NSInteger)programId { + return [self.dictionary[M3U8_EXT_X_STREAM_INF_PROGRAM_ID] integerValue]; +} + +- (NSArray*)codecs { + NSString* codecsString = self.dictionary[M3U8_EXT_X_STREAM_INF_CODECS]; + return [codecsString componentsSeparatedByString:@","]; +} + +- (MediaResoulution)resolution { + NSString* rStr = self.dictionary[M3U8_EXT_X_STREAM_INF_RESOLUTION]; + MediaResoulution resolution = MediaResolutionFromString(rStr); + return resolution; +} + +- (NSString*)audio { + return self.dictionary[M3U8_EXT_X_STREAM_INF_AUDIO]; +} + +- (NSString*)video { + return self.dictionary[M3U8_EXT_X_STREAM_INF_VIDEO]; +} + +- (NSString*)subtitles { + return self.dictionary[M3U8_EXT_X_STREAM_INF_SUBTITLES]; +} + +- (NSString*)closedCaptions { + return self.dictionary[M3U8_EXT_X_STREAM_INF_CLOSED_CAPTIONS]; +} + +- (NSURL*)URI { + NSString* uriString = [self.dictionary[M3U8_EXT_X_STREAM_INF_URI] stringByReplacingOccurrencesOfString:@" " withString:@"%20"]; + return [NSURL URLWithString:uriString]; +} + +- (NSString*)description { + return [NSString stringWithString:self.dictionary.description]; +} + +/* + #EXT-X-STREAM-INF:AUDIO="600k",BANDWIDTH=1049794,AVERAGE-BANDWIDTH=1000000,PROGRAM-ID=1,CODECS="avc1.42c01e,mp4a.40.2",RESOLUTION=640x360,SUBTITLES="subs" + main_media_0.m3u8 + */ +- (NSString*)m3u8PlainString { + NSMutableString* str = [NSMutableString string]; + [str appendString:M3U8_EXT_X_STREAM_INF]; + if (self.audio.length > 0) { + [str appendString:[NSString stringWithFormat:@"AUDIO=\"%@\"", self.audio]]; + [str appendString:[NSString stringWithFormat:@",BANDWIDTH=%ld", (long)self.bandwidth]]; + } else { + [str appendString:[NSString stringWithFormat:@"BANDWIDTH=%ld", (long)self.bandwidth]]; + } + + if (self.averageBandwidth > 0) { + [str appendString:[NSString stringWithFormat:@",AVERAGE-BANDWIDTH=%ld", (long)self.averageBandwidth]]; + } + + [str appendString:[NSString stringWithFormat:@",PROGRAM-ID=%ld", (long)self.programId]]; + NSString* codecsString = self.dictionary[M3U8_EXT_X_STREAM_INF_CODECS]; + [str appendString:[NSString stringWithFormat:@",CODECS=\"%@\"", codecsString]]; + NSString* rStr = self.dictionary[M3U8_EXT_X_STREAM_INF_RESOLUTION]; + if (rStr.length > 0) { + [str appendString:[NSString stringWithFormat:@",RESOLUTION=%@", rStr]]; + } + [str appendString:[NSString stringWithFormat:@"\n%@", self.URI.absoluteString]]; + return str; +} + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8ExtXStreamInfList.h b/ios/Video/M3U8Kit/Source/M3U8ExtXStreamInfList.h new file mode 100644 index 0000000000..c4786f4db2 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8ExtXStreamInfList.h @@ -0,0 +1,44 @@ +// +// M3U8ExtXStreamInfList.h +// ILSLoader +// +// Created by Jin Sun on 13-4-15. +// Copyright (c) 2013年 iLegendSoft. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8ExtXStreamInf.h" +#import + +@interface M3U8ExtXStreamInfList : NSObject + +@property(nonatomic, assign, readonly) NSUInteger count; + +- (void)addExtXStreamInf:(M3U8ExtXStreamInf*)extStreamInf; +- (M3U8ExtXStreamInf*)xStreamInfAtIndex:(NSUInteger)index; +- (M3U8ExtXStreamInf*)firstStreamInf; +- (M3U8ExtXStreamInf*)lastXStreamInf; + +- (void)sortByBandwidthInOrder:(NSComparisonResult)order; + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8ExtXStreamInfList.m b/ios/Video/M3U8Kit/Source/M3U8ExtXStreamInfList.m new file mode 100644 index 0000000000..fe53dddd36 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8ExtXStreamInfList.m @@ -0,0 +1,94 @@ +// +// M3U8ExtXStreamInfList.m +// ILSLoader +// +// Created by Jin Sun on 13-4-15. +// Copyright (c) 2013年 iLegendSoft. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +#import "M3U8ExtXStreamInfList.h" + +@interface M3U8ExtXStreamInfList () + +@property(nonatomic, strong) NSMutableArray* m3u8InfoList; + +@end + +@implementation M3U8ExtXStreamInfList + +- (id)init { + self = [super init]; + if (self) { + self.m3u8InfoList = [NSMutableArray array]; + } + + return self; +} + +#pragma mark - Getter && Setter +- (NSUInteger)count { + return [self.m3u8InfoList count]; +} + +#pragma mark - Public +- (void)addExtXStreamInf:(M3U8ExtXStreamInf*)extStreamInf { + [self.m3u8InfoList addObject:extStreamInf]; +} + +- (M3U8ExtXStreamInf*)xStreamInfAtIndex:(NSUInteger)index { + if (index >= self.count) { + return nil; + } + return [self.m3u8InfoList objectAtIndex:index]; +} + +- (M3U8ExtXStreamInf*)firstStreamInf { + return [self.m3u8InfoList firstObject]; +} + +- (M3U8ExtXStreamInf*)lastXStreamInf { + return [self.m3u8InfoList lastObject]; +} + +- (void)sortByBandwidthInOrder:(NSComparisonResult)order { + + NSArray* array = [self.m3u8InfoList sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) { + NSInteger bandwidth1 = ((M3U8ExtXStreamInf*)obj1).bandwidth; + NSInteger bandwidth2 = ((M3U8ExtXStreamInf*)obj2).bandwidth; + if (bandwidth1 == bandwidth2) { + return NSOrderedSame; + } else if (bandwidth1 < bandwidth2) { + return order; + } else { + return order * (-1); + } + }]; + + self.m3u8InfoList = [array mutableCopy]; +} + +- (NSString*)description { + return [NSString stringWithFormat:@"%@", self.m3u8InfoList]; +} + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8LineReader.h b/ios/Video/M3U8Kit/Source/M3U8LineReader.h new file mode 100644 index 0000000000..da2c430656 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8LineReader.h @@ -0,0 +1,39 @@ +// +// M3U8LineReader.h +// M3U8Kit +// +// Created by Noam Tamim on 22/03/2018. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import + +@interface M3U8LineReader : NSObject + +@property(nonatomic, readonly, strong) NSArray* lines; +@property(atomic, readonly, assign) NSUInteger index; + +- (instancetype)initWithText:(NSString*)text; +- (NSString*)next; + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8LineReader.m b/ios/Video/M3U8Kit/Source/M3U8LineReader.m new file mode 100644 index 0000000000..d283a2ae4a --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8LineReader.m @@ -0,0 +1,51 @@ +// +// M3U8LineReader.m +// M3U8Kit +// +// Created by Noam Tamim on 22/03/2018. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8LineReader.h" + +@implementation M3U8LineReader +- (instancetype)initWithText:(NSString*)text { + self = [super init]; + if (self) { + _lines = [text componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + } + return self; +} + +- (NSString*)next { + while (_index < _lines.count) { + NSString* line = [_lines[_index] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + _index++; + + if (line.length > 0) { + return line; + } + } + return nil; +} +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8MasterPlaylist.h b/ios/Video/M3U8Kit/Source/M3U8MasterPlaylist.h new file mode 100644 index 0000000000..0c3fde2157 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8MasterPlaylist.h @@ -0,0 +1,59 @@ +// +// M3U8MasterPlaylist.h +// M3U8Kit +// +// Created by Sun Jin on 3/25/14. +// Copyright (c) 2014 Jin Sun. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8ExtXKey.h" +#import "M3U8ExtXMediaList.h" +#import "M3U8ExtXStreamInfList.h" +#import + +@interface M3U8MasterPlaylist : NSObject + +@property(nonatomic, strong) NSString* name; + +@property(readonly, nonatomic, strong) NSString* version; + +@property(readonly, nonatomic, copy) NSString* originalText; +@property(readonly, nonatomic, copy) NSURL* baseURL; +@property(readonly, nonatomic, copy) NSURL* originalURL; + +@property(readonly, nonatomic, strong) M3U8ExtXKey* xSessionKey; + +@property(readonly, nonatomic, strong) M3U8ExtXStreamInfList* xStreamList; +@property(readonly, nonatomic, strong) M3U8ExtXMediaList* xMediaList; + +- (NSArray*)allStreamURLs; + +- (M3U8ExtXStreamInfList*)alternativeXStreamInfList; + +- (instancetype)initWithContent:(NSString*)string baseURL:(NSURL*)baseURL; +- (instancetype)initWithContentOfURL:(NSURL*)URL error:(NSError**)error; + +- (NSString*)m3u8PlainString; + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8MasterPlaylist.m b/ios/Video/M3U8Kit/Source/M3U8MasterPlaylist.m new file mode 100644 index 0000000000..8d76490810 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8MasterPlaylist.m @@ -0,0 +1,220 @@ +// +// M3U8MasterPlaylist.m +// M3U8Kit +// +// Created by Sun Jin on 3/25/14. +// Copyright (c) 2014 Jin Sun. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8MasterPlaylist.h" +#import "M3U8LineReader.h" +#import "M3U8TagsAndAttributes.h" +#import "NSString+m3u8.h" +#import "NSURL+m3u8.h" + +// #define M3U8_EXT_X_STREAM_INF_CLOSED_CAPTIONS @"CLOSED-CAPTIONS" // The value can be either a quoted-string or an enumerated-string +// with the value NONE. +// NSArray *quotedValueAttrs = @[@"URI", @"KEYFORMAT", @"KEYFORMATVERSIONS", @"GROUP-ID", @"LANGUAGE", @"ASSOC-LANGUAGE", @"NAME", +// @"INSTREAM-ID", @"CHARACTERISTICS", @"CODECS", @"AUDIO", @"VIDEO", @"SUBTITLES", @"BYTERANGE"]; + +@interface M3U8MasterPlaylist () + +@property(nonatomic, copy) NSString* originalText; +@property(nonatomic, copy) NSURL* baseURL; +@property(nonatomic, copy) NSURL* originalURL; + +@property(nonatomic, strong) NSString* version; + +@property(nonatomic, strong) M3U8ExtXKey* xSessionKey; + +@property(nonatomic, strong) M3U8ExtXStreamInfList* xStreamList; +@property(nonatomic, strong) M3U8ExtXMediaList* xMediaList; + +@end + +@implementation M3U8MasterPlaylist + +- (instancetype)initWithContent:(NSString*)string baseURL:(NSURL*)baseURL { + if (!string.m3u_isMasterPlaylist) { + return nil; + } + if (self = [super init]) { + self.originalText = string; + self.baseURL = baseURL; + [self parseMasterPlaylist]; + } + return self; +} + +- (instancetype)initWithContentOfURL:(NSURL*)URL error:(NSError**)error { + if (!URL) { + return nil; + } + + self.originalURL = URL; + + NSString* string = [NSString stringWithContentsOfURL:URL encoding:NSUTF8StringEncoding error:error]; + return [self initWithContent:string baseURL:URL.m3u_realBaseURL]; +} + +- (void)parseMasterPlaylist { + + self.xStreamList = [[M3U8ExtXStreamInfList alloc] init]; + self.xMediaList = [[M3U8ExtXMediaList alloc] init]; + + M3U8LineReader* reader = [[M3U8LineReader alloc] initWithText:self.originalText]; + + while (true) { + + NSString* line = [reader next]; + if (!line) { + break; + } + + // #EXT-X-VERSION:4 + if ([line hasPrefix:M3U8_EXT_X_VERSION]) { + NSRange r_version = [line rangeOfString:M3U8_EXT_X_VERSION]; + self.version = [line substringFromIndex:r_version.location + r_version.length]; + } + + else if ([line hasPrefix:M3U8_EXT_X_SESSION_KEY]) { + NSRange range = [line rangeOfString:M3U8_EXT_X_SESSION_KEY]; + NSString* attribute_list = [line substringFromIndex:range.location + range.length]; + NSMutableDictionary* attr = attribute_list.m3u_attributesFromAssignmentByComma; + + M3U8ExtXKey* sessionKey = [[M3U8ExtXKey alloc] initWithDictionary:attr]; + self.xSessionKey = sessionKey; + } + + // #EXT-X-STREAM-INF:AUDIO="600k",BANDWIDTH=915685,PROGRAM-ID=1,CODECS="avc1.42c01e,mp4a.40.2",RESOLUTION=640x360,SUBTITLES="subs" + // http://hls.ted.com/talks/769/video/600k.m3u8?sponsor=Ripple + else if ([line hasPrefix:M3U8_EXT_X_STREAM_INF]) { + NSRange range = [line rangeOfString:M3U8_EXT_X_STREAM_INF]; + NSString* attribute_list = [line substringFromIndex:range.location + range.length]; + NSMutableDictionary* attr = attribute_list.m3u_attributesFromAssignmentByComma; + + NSString* nextLine = [reader next]; + attr[@"URI"] = nextLine; + if (self.originalURL) { + attr[M3U8_URL] = self.originalURL; + } + + if (self.baseURL) { + attr[M3U8_BASE_URL] = self.baseURL; + } + + M3U8ExtXStreamInf* xStreamInf = [[M3U8ExtXStreamInf alloc] initWithDictionary:attr]; + [self.xStreamList addExtXStreamInf:xStreamInf]; + } + + // Ignore the following tag, which is not implemented yet. + // #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=65531,PROGRAM-ID=1,CODECS="avc1.42c00c",RESOLUTION=320x180,URI="/talks/769/video/64k_iframe.m3u8?sponsor=Ripple" + else if ([line hasPrefix:M3U8_EXT_X_I_FRAME_STREAM_INF]) { + + } + + // #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="600k",LANGUAGE="eng",NAME="Audio",AUTOSELECT=YES,DEFAULT=YES,URI="/talks/769/audio/600k.m3u8?sponsor=Ripple",BANDWIDTH=614400 + else if ([line hasPrefix:M3U8_EXT_X_MEDIA]) { + NSRange range = [line rangeOfString:M3U8_EXT_X_MEDIA]; + NSString* attribute_list = [line substringFromIndex:range.location + range.length]; + NSMutableDictionary* attr = attribute_list.m3u_attributesFromAssignmentByComma; + if (self.baseURL.absoluteString.length > 0) { + attr[M3U8_BASE_URL] = self.baseURL; + } + + if (self.originalURL.absoluteString.length > 0) { + attr[M3U8_URL] = self.originalURL; + } + M3U8ExtXMedia* media = [[M3U8ExtXMedia alloc] initWithDictionary:attr]; + [self.xMediaList addExtXMedia:media]; + } + } +} + +- (NSArray*)allStreamURLs { + NSMutableArray* array = [NSMutableArray array]; + for (int i = 0; i < self.xStreamList.count; i++) { + M3U8ExtXStreamInf* xsinf = [self.xStreamList xStreamInfAtIndex:i]; + if (xsinf.m3u8URL.absoluteString.length > 0) { + if (NO == [array containsObject:xsinf.m3u8URL]) { + [array addObject:xsinf.m3u8URL]; + } + } + } + return [array copy]; +} + +- (M3U8ExtXStreamInfList*)alternativeXStreamInfList { + + M3U8ExtXStreamInfList* list = [[M3U8ExtXStreamInfList alloc] init]; + + M3U8ExtXStreamInfList* xsilist = self.xStreamList; + for (int index = 0; index < xsilist.count; index++) { + M3U8ExtXStreamInf* xsinf = [xsilist xStreamInfAtIndex:index]; + BOOL flag = NO; + for (NSString* str in xsinf.codecs) { + if (NO == flag) { + flag = [str hasPrefix:@"avc1"]; + } + } + if (flag) { + [list addExtXStreamInf:xsinf]; + } + } + + // It is only used when the resolution is selected. + // M3U8ExtXMediaList *xmlist = self.masterPlaylist.xMediaList.videoList; + // for (int i = 0; i < xmlist.count; i ++) { + // M3U8ExtXMedia *media = [xmlist extXMediaAtIndex:i]; + // [allAlternativeURLStrings addObject:media.m3u8URL]; + // } + return list; +} + +- (NSString*)m3u8PlainString { + NSMutableString* str = [NSMutableString string]; + [str appendString:M3U8_EXTM3U]; + [str appendString:@"\n"]; + if (self.version.length > 0) { + [str appendString:[NSString stringWithFormat:@"%@%@", M3U8_EXT_X_VERSION, self.version]]; + [str appendString:@"\n"]; + } + for (NSInteger index = 0; index < self.xStreamList.count; index++) { + M3U8ExtXStreamInf* xsinf = [self.xStreamList xStreamInfAtIndex:index]; + [str appendString:xsinf.m3u8PlainString]; + [str appendString:@"\n"]; + } + + M3U8ExtXMediaList* audioList = self.xMediaList.audioList; + for (NSInteger i = 0; i < audioList.count; i++) { + NSLog(@"ext x media %ld", (long)i); + M3U8ExtXMedia* media = [audioList xMediaAtIndex:i]; + [str appendString:media.m3u8PlainString]; + [str appendString:@"\n"]; + } + + return str; +} + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8MediaPlaylist.h b/ios/Video/M3U8Kit/Source/M3U8MediaPlaylist.h new file mode 100644 index 0000000000..b46693b04e --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8MediaPlaylist.h @@ -0,0 +1,63 @@ +// +// M3U8MediaPlaylist.h +// M3U8Kit +// +// Created by Sun Jin on 3/26/14. +// Copyright (c) 2014 Jin Sun. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8SegmentInfoList.h" +#import + +typedef enum { + M3U8MediaPlaylistTypeMedia = 0, // The main media stream playlist. + M3U8MediaPlaylistTypeSubtitle = 1, // EXT-X-SUBTITLES TYPE=SUBTITLES + M3U8MediaPlaylistTypeAudio = 2, // EXT-X-MEDIA TYPE=AUDIO + M3U8MediaPlaylistTypeVideo = 3 // EXT-X-MEDIA TYPE=VIDEO +} M3U8MediaPlaylistType; + +@interface M3U8MediaPlaylist : NSObject + +@property(nonatomic, strong) NSString* name; + +@property(readonly, nonatomic, strong) NSString* version; + +@property(readonly, nonatomic, copy) NSString* originalText; +@property(readonly, nonatomic, copy) NSURL* baseURL; + +@property(readonly, nonatomic, copy) NSURL* originalURL; + +@property(readonly, nonatomic, strong) M3U8SegmentInfoList* segmentList; + +/** live or replay */ +@property(assign, readonly, nonatomic) BOOL isLive; + +@property(nonatomic) M3U8MediaPlaylistType type; // -1 by default + +- (instancetype)initWithContent:(NSString*)string type:(M3U8MediaPlaylistType)type baseURL:(NSURL*)baseURL; +- (instancetype)initWithContentOfURL:(NSURL*)URL type:(M3U8MediaPlaylistType)type error:(NSError**)error; + +- (NSArray*)allSegmentURLs; + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8MediaPlaylist.m b/ios/Video/M3U8Kit/Source/M3U8MediaPlaylist.m new file mode 100644 index 0000000000..8d77fe62c0 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8MediaPlaylist.m @@ -0,0 +1,169 @@ +// +// M3U8MediaPlaylist.m +// M3U8Kit +// +// Created by Sun Jin on 3/26/14. +// Copyright (c) 2014 Jin Sun. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8MediaPlaylist.h" +#import "M3U8ExtXByteRange.h" +#import "M3U8ExtXKey.h" +#import "M3U8LineReader.h" +#import "M3U8TagsAndAttributes.h" +#import "NSArray+m3u8.h" +#import "NSString+m3u8.h" +#import "NSURL+m3u8.h" + +@interface M3U8MediaPlaylist () + +@property(nonatomic, copy) NSString* originalText; +@property(nonatomic, copy) NSURL* baseURL; +@property(nonatomic, copy) NSURL* originalURL; + +@property(nonatomic, strong) NSString* version; + +@property(nonatomic, strong) M3U8SegmentInfoList* segmentList; + +@property(assign, nonatomic) BOOL isLive; + +@end + +@implementation M3U8MediaPlaylist + +- (instancetype)initWithContent:(NSString*)string type:(M3U8MediaPlaylistType)type baseURL:(NSURL*)baseURL { + if (!string.m3u_isMediaPlaylist) { + return nil; + } + + if (self = [super init]) { + self.originalText = string; + self.baseURL = baseURL; + self.type = type; + [self parseMediaPlaylist]; + } + return self; +} + +- (instancetype)initWithContentOfURL:(NSURL*)URL type:(M3U8MediaPlaylistType)type error:(NSError**)error { + if (nil == URL) { + return nil; + } + + self.originalURL = URL; + + NSString* string = [[NSString alloc] initWithContentsOfURL:URL encoding:NSUTF8StringEncoding error:error]; + + return [self initWithContent:string type:type baseURL:URL.m3u_realBaseURL]; +} + +- (NSArray*)allSegmentURLs { + NSMutableArray* array = [NSMutableArray array]; + for (int i = 0; i < self.segmentList.count; i++) { + M3U8SegmentInfo* info = [self.segmentList segmentInfoAtIndex:i]; + if (info.mediaURL.absoluteString.length > 0) { + if (NO == [array containsObject:info.mediaURL]) { + [array addObject:info.mediaURL]; + } + } + } + return [array copy]; +} + +- (void)parseMediaPlaylist { + self.segmentList = [[M3U8SegmentInfoList alloc] init]; + BOOL isLive = [self.originalText rangeOfString:M3U8_EXT_X_ENDLIST].location == NSNotFound; + self.isLive = isLive; + + M3U8LineReader* reader = [[M3U8LineReader alloc] initWithText:self.originalText]; + M3U8ExtXKey* key = nil; + + while (true) { + + NSString* line = [reader next]; + if (!line) { + break; + } + + NSMutableDictionary* params = [[NSMutableDictionary alloc] init]; + if (self.originalURL) { + [params setObject:self.originalURL forKey:M3U8_URL]; + } + + if (self.baseURL) { + [params setObject:self.baseURL forKey:M3U8_BASE_URL]; + } + + if ([line hasPrefix:M3U8_EXT_X_KEY]) { + line = [line stringByReplacingOccurrencesOfString:M3U8_EXT_X_KEY withString:@""]; + key = [[M3U8ExtXKey alloc] initWithDictionary:line.m3u_attributesFromAssignmentByComma]; + } + + // check if it's #EXTINF: + if ([line hasPrefix:M3U8_EXTINF]) { + line = [line stringByReplacingOccurrencesOfString:M3U8_EXTINF withString:@""]; + + NSArray* components = [line componentsSeparatedByString:@","]; + NSString* info = components.firstObject; + if (info) { + NSString* blankMark = @" "; + NSArray* additions = [info componentsSeparatedByString:blankMark]; + // get duration + NSString* duration = additions.firstObject; + params[M3U8_EXTINF_DURATION] = duration; + + // get additional parameters from Extended M3U https://en.wikipedia.org/wiki/M3U#Extended_M3U + if (additions.count > 1) { + // no need remove duration(first element). `m3u_attributesFromAssignmentByMark` function will skip first non-equation value. + params[M3U8_EXTINF_ADDITIONAL_PARAMETERS] = [additions m3u_attributesFromAssignmentByMark:blankMark]; + } + } + if (components.count > 1) { + params[M3U8_EXTINF_TITLE] = components[1]; + } + + line = reader.next; + // read ByteRange. only for version 4 + M3U8ExtXByteRange* byteRange = nil; + if ([line hasPrefix:M3U8_EXT_X_BYTERANGE]) { + line = [line stringByReplacingOccurrencesOfString:M3U8_EXT_X_BYTERANGE withString:@""]; + byteRange = [[M3U8ExtXByteRange alloc] initWithAtString:line]; + line = reader.next; + } + // ignore other # message + while ([line hasPrefix:@"#"]) { + line = reader.next; + } + // then get URI + params[M3U8_EXTINF_URI] = line; + + M3U8SegmentInfo* segment = [[M3U8SegmentInfo alloc] initWithDictionary:params xKey:key byteRange:byteRange]; + if (segment) { + [self.segmentList addSegementInfo:segment]; + } + } + } +} + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8Parser.h b/ios/Video/M3U8Kit/Source/M3U8Parser.h new file mode 100644 index 0000000000..85306e0e88 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8Parser.h @@ -0,0 +1,46 @@ +// +// M3U8Parser.h +// M3U8Parser +// +// Created by Frank on 20-4-16. +// Copyright (c) 2020年 M3U8Kit. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8ExtXKey.h" +#import "M3U8ExtXMedia.h" +#import "M3U8ExtXMediaList.h" +#import "M3U8ExtXStreamInf.h" +#import "M3U8ExtXStreamInfList.h" +#import "M3U8SegmentInfo.h" +#import "M3U8SegmentInfoList.h" + +#import "M3U8MasterPlaylist.h" +#import "M3U8MediaPlaylist.h" +#import "M3U8PlaylistModel.h" + +#import "M3U8ExtXByteRange.h" +#import "M3U8TagsAndAttributes.h" +#import "NSArray+m3u8.h" +#import "NSString+m3u8.h" +#import "NSURL+m3u8.h" diff --git a/ios/Video/M3U8Kit/Source/M3U8Parser.modulemap b/ios/Video/M3U8Kit/Source/M3U8Parser.modulemap new file mode 100644 index 0000000000..d3e6f04322 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8Parser.modulemap @@ -0,0 +1,28 @@ +//The MIT License (MIT) +// +//Copyright (c) 2015 Sun Jin +// +//Permission is hereby granted, free of charge, to any person obtaining a copy +//of this software and associated documentation files (the "Software"), to deal +//in the Software without restriction, including without limitation the rights +//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: +// +//The above copyright notice and this permission notice shall be included in all +//copies or substantial portions of the Software. +// +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +//SOFTWARE. + +framework module M3U8Parser { + umbrella header "M3U8Parser.h" + + export * + module * { export * } +} diff --git a/ios/Video/M3U8Kit/Source/M3U8PlaylistModel.h b/ios/Video/M3U8Kit/Source/M3U8PlaylistModel.h new file mode 100644 index 0000000000..abf25fe49b --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8PlaylistModel.h @@ -0,0 +1,98 @@ +// +// M3U8PlaylistModel.h +// M3U8Kit +// +// Created by Oneday on 13-1-11. +// Copyright (c) 2013年 0day. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8MasterPlaylist.h" +#import "M3U8MediaPlaylist.h" +#import + +// 用来管理 m3u playlist, 根据 URL 或者 string 生成 master playlist, 从master playlist 生成指定的 media playlist +// 生成 master playlist +// 提取默认的 media playlist,包括 video segments, subtitles playlist, audio playlist +// 取出media playlist 里面的链接等信息 + +// 把 master playlist 和 media playlist 中的链接替换 合成为本地服务器可用的playlist + +// 此版本为简化版,只考虑 #EXT-X-STREAM-INF Tag 中的信息,其他忽略掉 + +/** + + 需要下载的内容: + + --- master playlist 中的内容,如果有的话 + 1. master playlist + 2. 默认的 media playlist, 由第一个 #EXT-X-STREAM-INF Tag 指定 + 3. 音频 如果有一个 #EXT-X-STREAM-INF Tag 的codecs只有音频部分,则认为它的URI指向一个音频文件,应该下载下来,其它的Tag 暂时都简单的忽略掉 + + . 下载各 media playlist 对应的分段内容 + + */ + +@interface M3U8PlaylistModel : NSObject + +@property(readonly, nonatomic, copy) NSURL* baseURL; +@property(readonly, nonatomic, copy) NSURL* originalURL; + +// 如果初始化时的字符串是 media playlist, masterPlaylist为nil +// M3U8PlaylistModel 默认会按照《需要下载的内容》规则选取默认的playlist,如果需要手动指定获取特定的media playlist, 需调用方法 +// -specifyVideoURL:(这个在选取视频源的时候会用到); + +@property(readonly, nonatomic, strong) M3U8MasterPlaylist* masterPlaylist; + +@property(readonly, nonatomic, strong) M3U8MediaPlaylist* mainMediaPl; +- (void)changeMainMediaPlWithPlaylist:(M3U8MediaPlaylist*)playlist; +@property(readonly, nonatomic, strong) M3U8MediaPlaylist* audioPl; +//@property (readonly, nonatomic, strong) M3U8MediaPlaylist *subtitlePl; + +/** + this method is synchronous. so may be **block your thread** that call this method. + + @param URL M3U8 URL + @param error error pointer + @return playlist model + */ +- (id)initWithURL:(NSURL*)URL error:(NSError**)error; +- (id)initWithString:(NSString*)string baseURL:(NSURL*)URL error:(NSError**)error; +- (id)initWithString:(NSString*)string originalURL:(NSURL*)originalURL baseURL:(NSURL*)baseURL error:(NSError**)error; + +// 改变 mainMediaPl +// 这个url必须是master playlist 中多码率url(绝对地址)中的一个 +// 这个方法先会去获取url中的内容,生成一个mediaPlaylist,如果内容获取失败,mainMediaPl改变失败 +- (void)specifyVideoURL:(NSURL*)URL completion:(void (^)(BOOL success))completion; + +- (NSString*)indexPlaylistName; + +- (NSString*)prefixOfSegmentNameInPlaylist:(M3U8MediaPlaylist*)playlist; +- (NSString*)sufixOfSegmentNameInPlaylist:(M3U8MediaPlaylist*)playlist; + +- (NSArray*)segmentNamesForPlaylist:(M3U8MediaPlaylist*)playlist; + +// segment name will be formatted as ["%@%d.%@", prefix, index, sufix] eg. main_media_1.ts +- (void)savePlaylistsToPath:(NSString*)path error:(NSError**)error; + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8PlaylistModel.m b/ios/Video/M3U8Kit/Source/M3U8PlaylistModel.m new file mode 100644 index 0000000000..6adcad8931 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8PlaylistModel.m @@ -0,0 +1,314 @@ +// +// M3U8Parser.m +// M3U8Kit +// +// Created by Oneday on 13-1-11. +// Copyright (c) 2013年 0day. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8PlaylistModel.h" +#import "NSString+m3u8.h" +#import "NSURL+m3u8.h" + +#define INDEX_PLAYLIST_NAME @"index.m3u8" + +#define PREFIX_MAIN_MEDIA_PLAYLIST @"main_media_" +#define PREFIX_AUDIO_PLAYLIST @"x_media_audio_" +#define PREFIX_SUBTITLES_PLAYLIST @"x_media_subtitles_" + +@interface M3U8PlaylistModel () + +@property(nonatomic, copy) NSURL* baseURL; +@property(nonatomic, copy) NSURL* originalURL; + +@property(nonatomic, strong) M3U8MasterPlaylist* masterPlaylist; + +@property(nonatomic, strong) M3U8ExtXStreamInf* currentXStreamInf; + +@property(nonatomic, strong) M3U8MediaPlaylist* mainMediaPl; +@property(nonatomic, strong) M3U8MediaPlaylist* audioPl; +//@property (nonatomic, strong) M3U8MediaPlaylist *subtitlePl; + +@end + +@implementation M3U8PlaylistModel + +- (id)initWithURL:(NSURL*)URL error:(NSError**)error { + + NSString* str = [[NSString alloc] initWithContentsOfURL:URL encoding:NSUTF8StringEncoding error:error]; + if (*error) { + return nil; + } + + self.originalURL = URL; + + return [self initWithString:str baseURL:URL.m3u_realBaseURL error:error]; +} + +- (id)initWithString:(NSString*)string baseURL:(NSURL*)baseURL error:(NSError**)error { + return [self initWithString:string originalURL:nil baseURL:baseURL error:error]; +} + +- (id)initWithString:(NSString*)string originalURL:(NSURL*)originalURL baseURL:(NSURL*)baseURL error:(NSError**)error { + + if (!string.m3u_isExtendedM3Ufile) { + *error = [NSError errorWithDomain:@"M3U8PlaylistModel" + code:-998 + userInfo:@{NSLocalizedDescriptionKey : @"The content is not a m3u8 playlist"}]; + return nil; + } + + if (self = [super init]) { + if (string.m3u_isMasterPlaylist) { + self.originalURL = originalURL; + self.baseURL = baseURL; + self.masterPlaylist = [[M3U8MasterPlaylist alloc] initWithContent:string baseURL:baseURL]; + self.masterPlaylist.name = INDEX_PLAYLIST_NAME; + self.currentXStreamInf = self.masterPlaylist.xStreamList.firstStreamInf; + if (self.currentXStreamInf) { + NSError* ero; + NSURL* m3u8URL = self.currentXStreamInf.m3u8URL; + self.mainMediaPl = [[M3U8MediaPlaylist alloc] initWithContentOfURL:m3u8URL type:M3U8MediaPlaylistTypeMedia error:&ero]; + self.mainMediaPl.name = [NSString stringWithFormat:@"%@0.m3u8", PREFIX_MAIN_MEDIA_PLAYLIST]; + if (ero) { + NSLog(@"Get main media playlist failed, error: %@", ero); + } + } + + // get audioPl + M3U8ExtXStreamInfList* list = self.masterPlaylist.xStreamList; + if (list.count > 1) { + for (int i = 0; i < list.count; i++) { + M3U8ExtXStreamInf* xsinf = [list xStreamInfAtIndex:i]; + if (xsinf.codecs.count == 1 && [xsinf.codecs.firstObject hasPrefix:@"mp4a"]) { + NSURL* audioURL = xsinf.m3u8URL; + self.audioPl = [[M3U8MediaPlaylist alloc] initWithContentOfURL:audioURL type:M3U8MediaPlaylistTypeAudio error:NULL]; + self.audioPl.name = [NSString stringWithFormat:@"%@%d.m3u8", PREFIX_MAIN_MEDIA_PLAYLIST, i]; + break; + } + } + } + + } else if (string.m3u_isMediaPlaylist) { + self.mainMediaPl = [[M3U8MediaPlaylist alloc] initWithContent:string type:M3U8MediaPlaylistTypeMedia baseURL:baseURL]; + self.mainMediaPl.name = INDEX_PLAYLIST_NAME; + } + } + return self; +} + +- (NSSet*)allAlternativeURLStrings { + NSMutableSet* allAlternativeURLStrings = [NSMutableSet set]; + M3U8ExtXStreamInfList* xsilist = self.masterPlaylist.alternativeXStreamInfList; + for (int index = 0; index < xsilist.count; index++) { + M3U8ExtXStreamInf* xsinf = [xsilist xStreamInfAtIndex:index]; + [allAlternativeURLStrings addObject:xsinf.m3u8URL]; + } + + return allAlternativeURLStrings; +} + +- (void)specifyVideoURL:(NSURL*)URL completion:(void (^)(BOOL))completion { + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + BOOL success = NO; + + if (URL.absoluteString.length > 0 && nil != self.masterPlaylist && [self.allAlternativeURLStrings containsObject:URL]) { + + if ([URL.absoluteString isEqualToString:self.mainMediaPl.originalURL.absoluteString]) { + success = YES; + } else { + NSError* error; + M3U8MediaPlaylist* pl = [[M3U8MediaPlaylist alloc] initWithContentOfURL:URL type:M3U8MediaPlaylistTypeMedia error:&error]; + if (pl) { + self.mainMediaPl = pl; + M3U8ExtXStreamInfList* list = self.masterPlaylist.xStreamList; + if (list.count > 1) { + for (int i = 0; i < list.count; i++) { + M3U8ExtXStreamInf* xsinf = [list xStreamInfAtIndex:i]; + if ([xsinf.m3u8URL.absoluteString isEqualToString:pl.originalURL.absoluteString]) { + pl.name = [NSString stringWithFormat:@"%@%d.m3u8", PREFIX_MAIN_MEDIA_PLAYLIST, i]; + break; + } + } + } + success = YES; + } + } + } + + dispatch_async(dispatch_get_main_queue(), ^{ + if (completion) { + completion(success); + } + }); + }); +} + +- (void)changeMainMediaPlWithPlaylist:(M3U8MediaPlaylist*)playlist { + if (playlist && playlist.type == M3U8MediaPlaylistTypeMedia && [[self allAlternativeURLStrings] containsObject:playlist.baseURL]) { + + self.mainMediaPl = playlist; + M3U8ExtXStreamInfList* list = self.masterPlaylist.xStreamList; + if (list.count > 1) { + for (int i = 0; i < list.count; i++) { + M3U8ExtXStreamInf* xsinf = [list xStreamInfAtIndex:i]; + if ([xsinf.m3u8URL.absoluteString isEqualToString:playlist.originalURL.absoluteString]) { + playlist.name = [NSString stringWithFormat:@"%@%d.m3u8", PREFIX_MAIN_MEDIA_PLAYLIST, i]; + break; + } + } + } + } +} + +- (NSString*)prefixOfSegmentNameInPlaylist:(M3U8MediaPlaylist*)playlist { + NSString* prefix = nil; + + switch (playlist.type) { + case M3U8MediaPlaylistTypeMedia: + prefix = @"media_"; + break; + case M3U8MediaPlaylistTypeAudio: + prefix = @"audio_"; + break; + case M3U8MediaPlaylistTypeSubtitle: + prefix = @"subtitle_"; + break; + case M3U8MediaPlaylistTypeVideo: + prefix = @"video_"; + break; + + default: + return @""; + break; + } + return prefix; +} + +- (NSString*)sufixOfSegmentNameInPlaylist:(M3U8MediaPlaylist*)playlist { + NSString* prefix = nil; + + switch (playlist.type) { + case M3U8MediaPlaylistTypeMedia: + case M3U8MediaPlaylistTypeVideo: + prefix = @"ts"; + break; + case M3U8MediaPlaylistTypeAudio: + prefix = @"aac"; + break; + case M3U8MediaPlaylistTypeSubtitle: + prefix = @"vtt"; + break; + + default: + return @""; + break; + } + return prefix; +} + +- (NSArray*)segmentNamesForPlaylist:(M3U8MediaPlaylist*)playlist { + + NSString* prefix = [self prefixOfSegmentNameInPlaylist:playlist]; + NSString* sufix = [self sufixOfSegmentNameInPlaylist:playlist]; + NSMutableArray* names = [NSMutableArray array]; + + NSArray* URLs = playlist.allSegmentURLs; + NSUInteger count = playlist.segmentList.count; + NSUInteger index = 0; + for (int i = 0; i < count; i++) { + M3U8SegmentInfo* inf = [playlist.segmentList segmentInfoAtIndex:i]; + index = [URLs indexOfObject:inf.mediaURL]; + NSString* n = [NSString stringWithFormat:@"%@%lu.%@", prefix, (unsigned long)index, sufix]; + [names addObject:n]; + } + return names; +} + +- (void)savePlaylistsToPath:(NSString*)path error:(NSError**)error { + + if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + if (NO == [[NSFileManager defaultManager] removeItemAtPath:path error:error]) { + return; + } + } + if (NO == [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:error]) { + return; + } + + if (self.masterPlaylist) { + + // master playlist + NSString* masterContext = self.masterPlaylist.m3u8PlainString; + for (int i = 0; i < self.masterPlaylist.xStreamList.count; i++) { + M3U8ExtXStreamInf* xsinf = [self.masterPlaylist.xStreamList xStreamInfAtIndex:i]; + NSString* name = [NSString stringWithFormat:@"%@%d.m3u8", PREFIX_MAIN_MEDIA_PLAYLIST, i]; + masterContext = [masterContext stringByReplacingOccurrencesOfString:xsinf.URI.absoluteString withString:name]; + } + NSString* mPath = [path stringByAppendingPathComponent:self.indexPlaylistName]; + BOOL success = [masterContext writeToFile:mPath atomically:YES encoding:NSUTF8StringEncoding error:error]; + if (NO == success) { + NSLog(@"M3U8Kit Error: failed to save master playlist to file. error: %@", error ? *error : @"null"); + return; + } + + // main media playlist + [self saveMediaPlaylist:self.mainMediaPl toPath:path error:error]; + [self saveMediaPlaylist:self.audioPl toPath:path error:error]; + + } else { + [self saveMediaPlaylist:self.mainMediaPl toPath:path error:error]; + } +} + +- (void)saveMediaPlaylist:(M3U8MediaPlaylist*)playlist toPath:(NSString*)path error:(NSError**)error { + if (nil == playlist) { + return; + } + NSString* mainMediaPlContext = playlist.originalText; + if (mainMediaPlContext.length == 0) { + return; + } + + NSArray* names = [self segmentNamesForPlaylist:playlist]; + for (int i = 0; i < playlist.segmentList.count; i++) { + M3U8SegmentInfo* sinfo = [playlist.segmentList segmentInfoAtIndex:i]; + mainMediaPlContext = [mainMediaPlContext stringByReplacingOccurrencesOfString:sinfo.URI.absoluteString withString:names[i]]; + } + NSString* mainMediaPlPath = [path stringByAppendingPathComponent:playlist.name]; + BOOL success = [mainMediaPlContext writeToFile:mainMediaPlPath atomically:YES encoding:NSUTF8StringEncoding error:error]; + if (NO == success) { + if (NULL != error) { + NSLog(@"M3U8Kit Error: failed to save mian media playlist to file. error: %@", *error); + } + return; + } +} + +- (NSString*)indexPlaylistName { + return INDEX_PLAYLIST_NAME; +} + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8SegmentInfo.h b/ios/Video/M3U8Kit/Source/M3U8SegmentInfo.h new file mode 100644 index 0000000000..9c7278d1ef --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8SegmentInfo.h @@ -0,0 +1,68 @@ +// +// M3U8SegmentInfo.h +// M3U8Kit +// +// Created by Oneday on 13-1-11. +// Copyright (c) 2013年 0day. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import + +@class M3U8ExtXKey, M3U8ExtXByteRange; +/*! + @class M3U8SegmentInfo + @abstract This is the class indicates #EXTINF:, + media in m3u8 file + + +@format #EXTINF:<duration>,<title> + +#define M3U8_EXTINF @"#EXTINF:" + +#define M3U8_EXTINF_DURATION @"DURATION" +#define M3U8_EXTINF_TITLE @"TITLE" +#define M3U8_EXTINF_URI @"URI" +#define M3U8_EXT_X_KEY @"#EXT-X-KEY:" + + */ +@interface M3U8SegmentInfo : NSObject + +@property(readonly, nonatomic) NSTimeInterval duration; +@property(readonly, nonatomic, copy) NSString* title; +@property(readonly, nonatomic, copy) NSURL* URI; +@property(readonly, nonatomic, strong) M3U8ExtXByteRange* byteRange; +/** Key for media data decrytion. may be for this segment or next if no key. */ +@property(readonly, nonatomic, strong) M3U8ExtXKey* xKey; +@property(readonly, nonatomic, strong) NSDictionary<NSString*, NSString*>* additionalParameters; + +- (instancetype)initWithDictionary:(NSDictionary*)dictionary; + +- (instancetype)initWithDictionary:(NSDictionary*)dictionary xKey:(M3U8ExtXKey*)key; + +- (instancetype)initWithDictionary:(NSDictionary*)dictionary + xKey:(M3U8ExtXKey*)key + byteRange:(M3U8ExtXByteRange*)byteRange NS_DESIGNATED_INITIALIZER; + +- (NSURL*)mediaURL; + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8SegmentInfo.m b/ios/Video/M3U8Kit/Source/M3U8SegmentInfo.m new file mode 100644 index 0000000000..9074045e3d --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8SegmentInfo.m @@ -0,0 +1,105 @@ +// +// M3U8SegmentInfo.m +// M3U8Kit +// +// Created by Oneday on 13-1-11. +// Copyright (c) 2013年 0day. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin <jeansunvf@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8SegmentInfo.h" +#import "M3U8ExtXByteRange.h" +#import "M3U8ExtXKey.h" +#import "M3U8TagsAndAttributes.h" + +@interface M3U8SegmentInfo () +@property(nonatomic, strong) NSDictionary* dictionary; + +@end + +@implementation M3U8SegmentInfo + +@synthesize xKey = _xKey; + +- (instancetype)init { + return [self initWithDictionary:nil xKey:nil]; +} + +- (instancetype)initWithDictionary:(NSDictionary*)dictionary { + return [self initWithDictionary:dictionary xKey:nil]; +} + +- (instancetype)initWithDictionary:(NSDictionary*)dictionary xKey:(M3U8ExtXKey*)key { + return [self initWithDictionary:dictionary xKey:key byteRange:nil]; +} + +- (instancetype)initWithDictionary:(NSDictionary*)dictionary xKey:(M3U8ExtXKey*)key byteRange:(M3U8ExtXByteRange*)byteRange { + if (self = [super init]) { + _dictionary = dictionary; + _xKey = key; + _byteRange = byteRange; + } + return self; +} + +- (NSURL*)baseURL { + return self.dictionary[M3U8_BASE_URL]; +} + +- (NSURL*)URL { + return self.dictionary[M3U8_URL]; +} + +- (NSURL*)mediaURL { + if (self.URI.scheme) { + return self.URI; + } + + return [NSURL URLWithString:self.URI.absoluteString relativeToURL:[self baseURL]]; +} + +- (NSTimeInterval)duration { + return [self.dictionary[M3U8_EXTINF_DURATION] doubleValue]; +} + +- (NSString*)title { + return self.dictionary[M3U8_EXTINF_TITLE]; +} + +- (NSURL*)URI { + NSString* originalUrl = self.dictionary[M3U8_EXTINF_URI]; + NSString* urlString = [originalUrl stringByReplacingOccurrencesOfString:@" " withString:@"%20"]; + return [NSURL URLWithString:urlString]; +} + +- (NSDictionary<NSString*, NSString*>*)additionalParameters { + return self.dictionary[M3U8_EXTINF_ADDITIONAL_PARAMETERS]; +} + +- (NSString*)description { + NSMutableDictionary* dict = [self.dictionary mutableCopy]; + [dict addEntriesFromDictionary:[self.xKey valueForKey:@"dictionary"]]; + return dict.description; +} + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8SegmentInfoList.h b/ios/Video/M3U8Kit/Source/M3U8SegmentInfoList.h new file mode 100644 index 0000000000..890c863e7d --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8SegmentInfoList.h @@ -0,0 +1,40 @@ +// +// M3U8SegmentInfoList.h +// M3U8Kit +// +// Created by Oneday on 13-1-11. +// Copyright (c) 2013年 0day. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin <jeansunvf@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8SegmentInfo.h" +#import <Foundation/Foundation.h> + +@interface M3U8SegmentInfoList : NSObject + +@property(nonatomic, assign, readonly) NSUInteger count; + +- (void)addSegementInfo:(M3U8SegmentInfo*)segment; +- (M3U8SegmentInfo*)segmentInfoAtIndex:(NSUInteger)index; + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8SegmentInfoList.m b/ios/Video/M3U8Kit/Source/M3U8SegmentInfoList.m new file mode 100644 index 0000000000..75d693c6a3 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8SegmentInfoList.m @@ -0,0 +1,67 @@ +// +// M3U8SegmentInfoList.m +// M3U8Kit +// +// Created by Oneday on 13-1-11. +// Copyright (c) 2013年 0day. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin <jeansunvf@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8SegmentInfoList.h" + +@interface M3U8SegmentInfoList () + +@property(nonatomic, strong) NSMutableArray* segmentInfoList; + +@end + +@implementation M3U8SegmentInfoList + +- (id)init { + if (self = [super init]) { + self.segmentInfoList = [NSMutableArray array]; + } + return self; +} + +#pragma mark - Getter && Setter +- (NSUInteger)count { + return [self.segmentInfoList count]; +} + +#pragma mark - Public +- (void)addSegementInfo:(M3U8SegmentInfo*)segment { + if (segment) { + [self.segmentInfoList addObject:segment]; + } +} + +- (M3U8SegmentInfo*)segmentInfoAtIndex:(NSUInteger)index { + return [self.segmentInfoList objectAtIndex:index]; +} + +- (NSString*)description { + return [NSString stringWithFormat:@"%@", self.segmentInfoList]; +} + +@end diff --git a/ios/Video/M3U8Kit/Source/M3U8TagsAndAttributes.h b/ios/Video/M3U8Kit/Source/M3U8TagsAndAttributes.h new file mode 100644 index 0000000000..2dd6733f91 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/M3U8TagsAndAttributes.h @@ -0,0 +1,317 @@ +// +// M3U8TagsAndAttributes.h +// M3U8Kit +// +// Created by Sun Jin on 3/24/14. +// Copyright (c) 2014 Jin Sun. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin <jeansunvf@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// M3U8 Tags & Attributes definded in Draft Pantos Http Live Streaming 12 http://tools.ietf.org/html/draft-pantos-http-live-streaming-12 + +#define M3U8_URL @"URL" + +#define M3U8_BASE_URL @"baseURL" + +///------------------------------ +/// Standard Tags +///------------------------------ + +#define M3U8_EXTM3U @"#EXTM3U" + +/** + @format #EXTINF:<duration> <additional parameters> ...,<title> + */ +#define M3U8_EXTINF @"#EXTINF:" +#define M3U8_EXTINF_DURATION @"DURATION" +#define M3U8_EXTINF_TITLE @"TITLE" +#define M3U8_EXTINF_URI @"URI" +#define M3U8_EXTINF_ADDITIONAL_PARAMETERS @"ADDITIONAL_PARAMETERS" + +/// NEW TAGS +/** + @format #EXT-X-BYTERANGE:<n>[@<o>] + + @note The EXT-X-BYTERANGE tag appeared in version 4 of the protocol. It MUST NOT appear in a Master Playlist. + */ +#define M3U8_EXT_X_BYTERANGE @"#EXT-X-BYTERANGE:" + +/** + @format #EXT-X-TARGETDURATION:<s> + + @note The EXT-X-TARGETDURATION tag MUST NOT appear in a Master Playlist. + */ +#define M3U8_EXT_X_TARGETDURATION @"#EXT-X-TARGETDURATION:" + +/** + @format #EXT-X-MEDIA-SEQUENCE:<number> + @note The EXT-X-MEDIA-SEQUENCE tag MUST NOT appear in a Master Playlist. + */ +#define M3U8_EXT_X_MEDIA_SEQUENCE @"#EXT-X-MEDIA-SEQUENCE:" + +/// EXT-X-KEY +/** + @format #EXT-X-KEY:<attribute-list> ps: We may need download the key file at URI. + */ +#define M3U8_EXT_X_KEY @"#EXT-X-KEY:" +// EXT-X-KEY Attributes +#define M3U8_EXT_X_KEY_METHOD \ + @"METHOD" // The value is an enumerated-string that specifies the encryption method. This attribute is REQUIRED. + // The methods defined are: NONE, AES-128, and SAMPLE-AES. +#define M3U8_EXT_X_KEY_URI \ + @"URI" // The value is a quoted-string containing a URI [RFC3986] that specifies how to obtain the key. This attribute is REQUIRED + // unless the METHOD is NONE. +#define M3U8_EXT_X_KEY_IV @"IV" // The value is a hexadecimal-integer that specifies the Initialization Vector to be used with the key. +#define M3U8_EXT_X_KEY_KEYFORMAT \ + @"KEYFORMAT" // The value is a quoted-string that specifies how the key is represented in the resource identified by the URI +#define M3U8_EXT_X_KEY_KEYFORMATVERSIONS \ + @"KEYFORMATVERSIONS" // The value is a quoted-string containing one or more positive integers separated by the "/" character (for example, + // "1/3"). + +/// M3U8_EXT_X_SESSION_KEY +/** + Preload EXT-X-KEY infomations + + @format #EXT-X-SESSION-KEY:<attribute list> + + All attributes defined for the EXT-X-KEY tag (Section 4.3.2.4) are + also defined for the EXT-X-SESSION-KEY, except that the value of the + METHOD attribute MUST NOT be NONE. If an EXT-X-SESSION-KEY is used, + the values of the METHOD, KEYFORMAT and KEYFORMATVERSIONS attributes + MUST match any EXT-X-KEY with the same URI value. + + EXT-X-SESSION-KEY tags SHOULD be added if multiple Variant Streams or + Renditions use the same encryption keys and formats. A EXT-X + -SESSION-KEY tag is not associated with any particular Media + Playlist. + + A Master Playlist MUST NOT contain more than one EXT-X-SESSION-KEY + tag with the same METHOD, URI, IV, KEYFORMAT, and KEYFORMATVERSIONS + attribute values. + + The EXT-X-SESSION-KEY tag is optional.. + */ +#define M3U8_EXT_X_SESSION_KEY @"#EXT-X-SESSION-KEY:" + +/// M3U8_EXT_X_SESSION_DATA +/** + Some customized data between Server & Client + The EXT-X-SESSION-DATA tag allows arbitrary session data to be + carried in a Master Playlist. + + @format #EXT-X-SESSION-DATA:<attribute list> . + @example #EXT-X-SESSION-DATA:DATA-ID="com.example.lyrics",URI="lyrics.json" + @example #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" + + + The following attributes are defined: + + DATA-ID + + The value of DATA-ID is a quoted-string which identifies that data + value. The DATA-ID SHOULD conform to a reverse DNS naming + convention, such as "com.example.movie.title"; however, there is + no central registration authority, so Playlist authors SHOULD take + care to choose a value which is unlikely to collide with others. + This attribute is REQUIRED. + + VALUE + + VALUE is a quoted-string. It contains the data identified by + DATA-ID. If the LANGUAGE is specified, VALUE SHOULD contain a + human-readable string written in the specified language. + + URI + + The value is a quoted-string containing a URI. The resource + identified by the URI MUST be formatted as JSON [RFC7159]; + otherwise, clients may fail to interpret the resource. + + LANGUAGE + + The value is a quoted-string containing a language tag [RFC5646] + that identifies the language of the VALUE. This attribute is + OPTIONAL. + + Each EXT-X-SESSION-DATA tag MUST contain either a VALUE or URI + attribute, but not both. + + A Playlist MAY contain multiple EXT-X-SESSION-DATA tags with the same + DATA-ID attribute. A Playlist MUST NOT contain more than one EXT-X + -SESSION-DATA tag with the same DATA-ID attribute and the same + LANGUAGE attribute. + */ +#define M3U8_EXT_X_SESSION_DATA @"EXT-X-SESSION-DATA:" + +/** + @format: #EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ> + @note The EXT-X-PROGRAM-DATE-TIME tag MUST NOT appear in a Master Playlist. + */ +#define M3U8_EXT_X_PROGRAM_DATE_TIME @"#EXT-X-PROGRAM-DATE-TIME:" + +/** + @format: #EXT-X-ALLOW-CACHE:<YES|NO> + @note It MUST NOT occur more than once. + */ +#define M3U8_EXT_X_ALLOW_CACHE @"#EXT-X-ALLOW-CACHE:" + +/** + @format: #EXT-X-PLAYLIST-TYPE:<EVENT|VOD> + @note The EXT-X-PLAYLIST-TYPE tag MUST NOT appear in a Master Playlist. + */ +#define M3U8_EXT_X_PLAYLIST_TYPE @"#EXT-X-PLAYLIST-TYPE:" + +/** + @format: #EXT-X-ENDLIST ps: it MUST NOT occur more than once. + @note The EXT-X-ENDLIST tag MUST NOT appear in a Master Playlist. + */ +#define M3U8_EXT_X_ENDLIST @"#EXT-X-ENDLIST" + +/// EXT-X-MEDIA +/** + @format #EXT-X-MEDIA:<attribute-list> , attibute-list: ATTR=<value>,... + @example + #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="600k",LANGUAGE="eng",NAME="Audio",AUTOSELECT=YES,DEFAULT=YES,URI="/talks/769/audio/600k.m3u8?sponsor=Ripple",BANDWIDTH=614400 + */ +#define M3U8_EXT_X_MEDIA @"#EXT-X-MEDIA:" +// EXT-X-MEDIA attributes +#define M3U8_EXT_X_MEDIA_TYPE @"TYPE" // The value is enumerated-string; valid strings are AUDIO, VIDEO, SUBTITLES and CLOSED-CAPTIONS. +#define M3U8_EXT_X_MEDIA_URI @"URI" // The value is a quoted-string containing a URI that identifies the Playlist file. +#define M3U8_EXT_X_MEDIA_GROUP_ID @"GROUP-ID" // The value is a quoted-string identifying a mutually-exclusive group of renditions. +#define M3U8_EXT_X_MEDIA_LANGUAGE \ + @"LANGUAGE" // The value is a quoted-string containing an RFC 5646 [RFC5646] language tag that identifies the primary language used in the + // rendition. +#define M3U8_EXT_X_MEDIA_ASSOC_LANGUAGE \ + @"ASSOC-LANGUAGE" // The value is a quoted-string containing an RFC 5646 [RFC5646](http://tools.ietf.org/html/rfc5646) language tag that + // identifies a language that is associated with the rendition. +#define M3U8_EXT_X_MEDIA_NAME @"NAME" // The value is a quoted-string containing a human-readable description of the rendition. +#define M3U8_EXT_X_MEDIA_DEFAULT @"DEFAULT" // The value is an enumerated-string; valid strings are YES and NO. +#define M3U8_EXT_X_MEDIA_AUTOSELECT @"AUTOSELECT" // The value is an enumerated-string; valid strings are YES and NO. +#define M3U8_EXT_X_MEDIA_FORCED @"FORCED" // The value is an enumerated-string; valid strings are YES and NO. +#define M3U8_EXT_X_MEDIA_INSTREAM_ID \ + @"INSTREAM-ID" // The value is a quoted-string that specifies a rendition within the segments in the Media Playlist. +#define M3U8_EXT_X_MEDIA_CHARACTERISTICS \ + @"CHARACTERISTICS" // The value is a quoted-string containing one or more Uniform Type Identifiers [UTI] separated by comma (,) + // characters. +#define M3U8_EXT_X_MEDIA_BANDWIDTH @"BANDWIDTH" +#define M3U8_EXT_X_MEDIA_CHANNELS @"CHANNELS" + +/// EXT-X-STREAM-INF +/** + @format #EXT-X-STREAM-INF:<attribute-list> + <URI> + @example #EXT-X-STREAM-INF:AUDIO="600k",BANDWIDTH=915685,PROGRAM-ID=1,CODECS="avc1.42c01e,mp4a.40.2",RESOLUTION=640x360,SUBTITLES="subs" + /talks/769/video/600k.m3u8?sponsor=Ripple + + @note The EXT-X-STREAM-INF tag MUST NOT appear in a Media Playlist. + */ +#define M3U8_EXT_X_STREAM_INF @"#EXT-X-STREAM-INF:" +// EXT-X-STREAM-INF Attributes +#define M3U8_EXT_X_STREAM_INF_BANDWIDTH @"BANDWIDTH" // The value is a decimal-integer of bits per second. +#define M3U8_EXT_X_STREAM_INF_AVERAGE_BANDWIDTH \ + @"AVERAGE-BANDWIDTH" // The value is a decimal-integer of bits per second. It represents the average segment bit rate of the Variant + // Stream. +#define M3U8_EXT_X_STREAM_INF_PROGRAM_ID \ + @"PROGRAM-ID" // The value is a decimal-integer that uniquely identifies a particular presentation within the scope of the Playlist file. +#define M3U8_EXT_X_STREAM_INF_CODECS @"CODECS" // The value is a quoted-string containing a comma-separated list of formats. +#define M3U8_EXT_X_STREAM_INF_RESOLUTION \ + @"RESOLUTION" // The value is a decimal-resolution describing the approximate encoded horizontal and vertical resolution of video within + // the presentation. +#define M3U8_EXT_X_STREAM_INF_AUDIO @"AUDIO" // The value is a quoted-string. +#define M3U8_EXT_X_STREAM_INF_VIDEO @"VIDEO" // The value is a quoted-string. +#define M3U8_EXT_X_STREAM_INF_SUBTITLES @"SUBTITLES" // The value is a quoted-string. +#define M3U8_EXT_X_STREAM_INF_CLOSED_CAPTIONS \ + @"CLOSED-CAPTIONS" // The value can be either a quoted-string or an enumerated-string with the value NONE. +#define M3U8_EXT_X_STREAM_INF_URI @"URI" // The value is a enumerated-string containing a URI that identifies the Playlist file. + +/** + @format #EXT-X-DISCONTINUITY + @note The EXT-X-DISCONTINUITY tag MUST NOT appear in a Master Playlist. + */ +#define M3U8_EXT_X_DISCONTINUITY @"#EXT-X-DISCONTINUITY" + +/** + @format #EXT-X-DISCONTINUITY-SEQUENCE:<number> where number is a decimal-integer. + @note The discontinuity sequence number MUST NOT decrease. + A Media Playlist MUST NOT contain more than one EXT-X-DISCONTINUITY-SEQUENCE tag. + */ +#define M3U8_EXT_X_DISCONTINUITY_SEQUENCE @"#EXT-X-DISCONTINUITY-SEQUENCE:" + +/** + @format #EXT-X-I-FRAMES-ONLY + @note The EXT-X-I-FRAMES-ONLY tag MUST NOT appear in a Master Playlist.(v4) + */ +#define M3U8_EXT_X_I_FRAMES_ONLY @"#EXT-X-I-FRAMES-ONLY" + +/// EXT-X-MAP +/** + @format #EXT-X-MAP:<attribute-list> + @note The EXT-X-MAP tag MUST NOT appear in a Master Playlist. + */ +#define M3U8_EXT_X_MAP @"#EXT-X-MAP:" +// EXT-X-MAP attributes +#define M3U8_EXT_X_MAP_URI \ + @"URI" // The value is a quoted-string containing a URI that identifies a resource that contains segment header information. This + // attribute is REQUIRED. +#define M3U8_EXT_X_MAP_BYTERANGE \ + @"BYTERANGE" // The value is a quoted-string specifying a byte range into the resource identified by the URI attribute. + +/// EXT-X-I-FRAME-STREAM-INF +/** + @format #EXT-X-I-FRAME-STREAM-INF:<attribute-list> + @example + #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=65531,PROGRAM-ID=1,CODECS="avc1.42c00c",RESOLUTION=320x180,URI="/talks/769/video/64k_iframe.m3u8?sponsor=Ripple" + + @note All attributes defined for the EXT-X-STREAM-INF tag (Section 3.4.10) are also defined for the EXT-X-I-FRAME-STREAM-INF tag, + except for the AUDIO, SUBTITLES and CLOSED-CAPTIONS attributes. The EXT-X-I-FRAME-STREAM-INF tag MUST NOT appear in a Media Playlist. + */ +#define M3U8_EXT_X_I_FRAME_STREAM_INF @"#EXT-X-I-FRAME-STREAM-INF:" +// EXT-X-I-FRAME-STREAM-INF Attributes +#define M3U8_EXT_X_I_FRAME_STREAM_INF_URI @"URI" // The value is a quoted-string containing a URI that identifies the I-frame Playlist file. +#define M3U8_EXT_X_I_FRAME_STREAM_INF_BANDWIDTH @"BANDWIDTH" // The value is a decimal-integer of bits per second. +#define M3U8_EXT_X_I_FRAME_STREAM_INF_PROGRAM_ID \ + @"PROGRAM-ID" // The value is a decimal-integer that uniquely identifies a particular presentation within the scope of the Playlist file. +#define M3U8_EXT_X_I_FRAME_STREAM_INF_CODECS @"CODECS" // The value is a quoted-string containing a comma-separated list of formats. +#define M3U8_EXT_X_I_FRAME_STREAM_INF_RESOLUTION \ + @"RESOLUTION" // The value is a decimal-resolution describing the approximate encoded horizontal and vertical resolution of video within + // the presentation. +#define M3U8_EXT_X_I_FRAME_STREAM_INF_VIDEO @"VIDEO" // The value is a quoted-string. + +/// EXT-X-START +/** + @format #EXT-X-START:<attribute list> + */ +#define M3U8_EXT_X_START @"#EXT-X-START:" +// EXT-X-START Attributes +#define M3U8_EXT_X_START_TIME_OFFSET @"TIME-OFFSET" // The value of TIME-OFFSET is a decimal-floating-point number of seconds. +#define M3U8_EXT_X_START_PRECISE @"PRECISE" // The value is an enumerated-string; valid strings are YES and NO. + +/** + @format #EXT-X-VERSION:<n> where n is an integer indicating the protocol version. + */ +#define M3U8_EXT_X_VERSION @"#EXT-X-VERSION:" + +/** + @format #EXT-X-SESSION-KEY:<n> where n is an integer indicating the protocol version. + */ +#define M3U8_EXT_X_SESSION_KEY @"#EXT-X-SESSION-KEY:" diff --git a/ios/Video/M3U8Kit/Source/NSArray+m3u8.h b/ios/Video/M3U8Kit/Source/NSArray+m3u8.h new file mode 100644 index 0000000000..3808d9a849 --- /dev/null +++ b/ios/Video/M3U8Kit/Source/NSArray+m3u8.h @@ -0,0 +1,49 @@ +// +// NSArray+m3u8.h +// M3U8Kit +// +// Created by Frank on 2022/7/12. +// Copyright © 2022 M3U8Kit. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin <jeansunvf@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface NSArray (m3u8) + +/** @return "key=value" transform to dictionary */ +- (NSMutableDictionary*)m3u_attributesFromAssignment; + +/** + If check by invalid value, value will append to last element with specific mark. + + @param mark attribute will be ignored if it is invalid. + @return "key=value" transform to dictionary + */ +- (NSMutableDictionary*)m3u_attributesFromAssignmentByMark:(nullable NSString*)mark; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/Video/M3U8Kit/Source/NSArray+m3u8.m b/ios/Video/M3U8Kit/Source/NSArray+m3u8.m new file mode 100644 index 0000000000..da9994d24b --- /dev/null +++ b/ios/Video/M3U8Kit/Source/NSArray+m3u8.m @@ -0,0 +1,66 @@ +// +// NSArray+m3u8.m +// M3U8Kit +// +// Created by Frank on 2022/7/12. +// Copyright © 2022 M3U8Kit. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin <jeansunvf@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "NSArray+m3u8.h" +#import "NSString+m3u8.h" + +@implementation NSArray (m3u8) + +- (NSMutableDictionary*)m3u_attributesFromAssignment { + return [self m3u_attributesFromAssignmentByMark:nil]; +} + +- (NSMutableDictionary*)m3u_attributesFromAssignmentByMark:(NSString*)mark { + NSMutableDictionary* dict = [NSMutableDictionary dictionary]; + + NSString* lastkey = nil; + for (NSString* keyValue in self) { + NSRange equalMarkRange = [keyValue rangeOfString:@"="]; + // if equal mark is not found, it means this value is previous value left. eg: CODECS=\"avc1.42c01e,mp4a.40.2\" + if (equalMarkRange.location == NSNotFound) { + if (!mark) + continue; + if (!lastkey) + continue; + NSString* lastValue = dict[lastkey]; + NSString* supplement = [lastValue stringByAppendingFormat:@"%@%@", mark, keyValue.m3u_stringByTrimmingQuoteMark]; + dict[lastkey] = supplement; + continue; + } + NSString* key = [keyValue substringToIndex:equalMarkRange.location].m3u_stringByTrimmingQuoteMark; + NSString* value = [keyValue substringFromIndex:equalMarkRange.location + 1].m3u_stringByTrimmingQuoteMark; + + dict[key] = value; + lastkey = key; + } + + return dict; +} + +@end diff --git a/ios/Video/M3U8Kit/Source/NSString+m3u8.h b/ios/Video/M3U8Kit/Source/NSString+m3u8.h new file mode 100644 index 0000000000..bf2ca100bb --- /dev/null +++ b/ios/Video/M3U8Kit/Source/NSString+m3u8.h @@ -0,0 +1,51 @@ +// +// NSString+m3u8.h +// M3U8Kit +// +// Created by Oneday on 13-1-11. +// Copyright (c) 2013年 0day. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin <jeansunvf@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import <Foundation/Foundation.h> + +@class M3U8ExtXStreamInfList, M3U8SegmentInfoList; +@interface NSString (m3u8) + +- (BOOL)m3u_isExtendedM3Ufile; + +- (BOOL)m3u_isMasterPlaylist; +- (BOOL)m3u_isMediaPlaylist; + +- (M3U8SegmentInfoList*)m3u_segementInfoListValueRelativeToURL:(NSString*)baseURL; + +/** + @return "key=value" transform to dictionary + */ +- (NSMutableDictionary*)m3u_attributesFromAssignmentByMark:(NSString*)mark; +- (NSMutableDictionary*)m3u_attributesFromAssignmentByComma; +- (NSMutableDictionary*)m3u_attributesFromAssignmentByBlank; + +- (NSString*)m3u_stringByTrimmingQuoteMark; + +@end diff --git a/ios/Video/M3U8Kit/Source/NSString+m3u8.m b/ios/Video/M3U8Kit/Source/NSString+m3u8.m new file mode 100644 index 0000000000..f93c5bd4ae --- /dev/null +++ b/ios/Video/M3U8Kit/Source/NSString+m3u8.m @@ -0,0 +1,168 @@ +// +// NSString+m3u8.m +// M3U8Kit +// +// Created by Oneday on 13-1-11. +// Copyright (c) 2013年 0day. All rights reserved. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin <jeansunvf@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8ExtXStreamInf.h" +#import "M3U8ExtXStreamInfList.h" +#import "M3U8SegmentInfo.h" +#import "M3U8SegmentInfoList.h" +#import "NSString+m3u8.h" + +#import "M3U8TagsAndAttributes.h" +#import "NSArray+m3u8.h" + +@implementation NSString (m3u8) + +/** + The Extended M3U file format defines two tags: EXTM3U and EXTINF. An + Extended M3U file is distinguished from a basic M3U file by its first + line, which MUST be #EXTM3U. + + reference url:http://tools.ietf.org/html/draft-pantos-http-live-streaming-00 + */ +- (BOOL)m3u_isExtendedM3Ufile { + return [self hasPrefix:M3U8_EXTM3U]; +} + +- (BOOL)m3u_isMasterPlaylist { + BOOL isM3U = [self m3u_isExtendedM3Ufile]; + if (isM3U) { + NSRange r1 = [self rangeOfString:M3U8_EXT_X_STREAM_INF]; + NSRange r2 = [self rangeOfString:M3U8_EXT_X_I_FRAME_STREAM_INF]; + if (r1.location != NSNotFound || r2.location != NSNotFound) { + return YES; + } + } + return NO; +} + +- (BOOL)m3u_isMediaPlaylist { + BOOL isM3U = [self m3u_isExtendedM3Ufile]; + if (isM3U) { + NSRange r = [self rangeOfString:M3U8_EXTINF]; + if (r.location != NSNotFound) { + return YES; + } + } + return NO; +} + +- (M3U8SegmentInfoList*)m3u_segementInfoListValueRelativeToURL:(NSString*)baseURL { + // self == @"" + if (0 == self.length) + return nil; + + /** + The Extended M3U file format defines two tags: EXTM3U and EXTINF. An + Extended M3U file is distinguished from a basic M3U file by its first + line, which MUST be #EXTM3U. + + reference url:http://tools.ietf.org/html/draft-pantos-http-live-streaming-00 + */ + NSRange rangeOfEXTM3U = [self rangeOfString:M3U8_EXTM3U]; + if (rangeOfEXTM3U.location == NSNotFound || rangeOfEXTM3U.location != 0) { + return nil; + } + + M3U8SegmentInfoList* segmentInfoList = [[M3U8SegmentInfoList alloc] init]; + + NSRange segmentRange = [self rangeOfString:M3U8_EXTINF]; + NSString* remainingSegments = self; + + while (NSNotFound != segmentRange.location) { + NSMutableDictionary* params = [[NSMutableDictionary alloc] init]; + if (baseURL) { + [params setObject:baseURL forKey:M3U8_BASE_URL]; + } + + // Read the EXTINF number between #EXTINF: and the comma + NSRange commaRange = [remainingSegments rangeOfString:@","]; + NSRange valueRange = NSMakeRange(segmentRange.location + 8, commaRange.location - (segmentRange.location + 8)); + if (commaRange.location == NSNotFound || valueRange.location > remainingSegments.length - 1) + break; + + NSString* value = [remainingSegments substringWithRange:valueRange]; + [params setValue:value forKey:M3U8_EXTINF_DURATION]; + + // ignore the #EXTINF line + remainingSegments = [remainingSegments substringFromIndex:segmentRange.location]; + NSRange extinfoLFRange = [remainingSegments rangeOfString:@"\n"]; + remainingSegments = [remainingSegments substringFromIndex:extinfoLFRange.location + 1]; + + // Read the segment link, and ignore line start with # && blank line + while (1) { + NSRange lfRange = [remainingSegments rangeOfString:@"\n"]; + NSString* line = [remainingSegments substringWithRange:NSMakeRange(0, lfRange.location)]; + line = [line stringByReplacingOccurrencesOfString:@" " withString:@""]; + + remainingSegments = [remainingSegments substringFromIndex:lfRange.location + 1]; + + if ([line characterAtIndex:0] != '#' && 0 != line.length) { + // remove the CR character '\r' + unichar lastChar = [line characterAtIndex:line.length - 1]; + if (lastChar == '\r') { + line = [line substringToIndex:line.length - 1]; + } + + [params setValue:line forKey:M3U8_EXTINF_URI]; + break; + } + } + + M3U8SegmentInfo* segment = [[M3U8SegmentInfo alloc] initWithDictionary:params]; + if (segment) { + [segmentInfoList addSegementInfo:segment]; + } + + segmentRange = [remainingSegments rangeOfString:M3U8_EXTINF]; + } + + return segmentInfoList; +} + +- (NSString*)m3u_stringByTrimmingQuoteMark { + NSCharacterSet* quoteMarkCharactersSet = [NSCharacterSet characterSetWithCharactersInString:@"\"' "]; + NSString* string = [self stringByTrimmingCharactersInSet:quoteMarkCharactersSet]; + return string; +} + +- (NSMutableDictionary*)m3u_attributesFromAssignmentByComma { + return [self m3u_attributesFromAssignmentByMark:@","]; +} + +- (NSMutableDictionary*)m3u_attributesFromAssignmentByBlank { + return [self m3u_attributesFromAssignmentByMark:@" "]; +} + +- (NSMutableDictionary*)m3u_attributesFromAssignmentByMark:(NSString*)mark { + NSArray<NSString*>* keyValues = [self componentsSeparatedByString:mark]; + + return [keyValues m3u_attributesFromAssignmentByMark:mark]; +} + +@end diff --git a/ios/Video/M3U8Kit/Source/NSURL+m3u8.h b/ios/Video/M3U8Kit/Source/NSURL+m3u8.h new file mode 100644 index 0000000000..0b715cd5aa --- /dev/null +++ b/ios/Video/M3U8Kit/Source/NSURL+m3u8.h @@ -0,0 +1,49 @@ +// +// NSURL+m3u8.h +// M3U8Kit +// +// Created by Frank on 16/06/2017. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin <jeansunvf@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import <Foundation/Foundation.h> + +@class M3U8PlaylistModel; +@interface NSURL (m3u8) + +/** + return baseURL if exists. + if baseURL is nil, return [scheme://host] + + @return URL + */ +- (NSURL*)m3u_realBaseURL; + +/** + Load the specific url and get result model with completion block. + + @param completion when the url resource loaded, completion block could get model and detail error; + */ +- (void)m3u_loadAsyncCompletion:(void (^)(M3U8PlaylistModel* model, NSError* error))completion; + +@end diff --git a/ios/Video/M3U8Kit/Source/NSURL+m3u8.m b/ios/Video/M3U8Kit/Source/NSURL+m3u8.m new file mode 100644 index 0000000000..c8839ac48c --- /dev/null +++ b/ios/Video/M3U8Kit/Source/NSURL+m3u8.m @@ -0,0 +1,65 @@ +// +// NSURL+m3u8.m +// M3U8Kit +// +// Created by Frank on 16/06/2017. +// +// The MIT License (MIT) +// +// Copyright (c) 2015 Sun Jin <jeansunvf@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#import "M3U8PlaylistModel.h" +#import "NSURL+m3u8.h" + +@implementation NSURL (m3u8) + +- (NSURL*)m3u_realBaseURL { + NSURL* baseURL = self.baseURL; + if (!baseURL) { + NSString* string = [self.absoluteString stringByReplacingOccurrencesOfString:self.lastPathComponent withString:@""]; + + baseURL = [NSURL URLWithString:string]; + } + + return baseURL; +} + +- (void)m3u_loadAsyncCompletion:(void (^)(M3U8PlaylistModel* model, NSError* error))completion { + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ + NSError* err = nil; + NSString* str = [[NSString alloc] initWithContentsOfURL:self encoding:NSUTF8StringEncoding error:&err]; + + if (err) { + completion(nil, err); + return; + } + + M3U8PlaylistModel* listModel = [[M3U8PlaylistModel alloc] initWithString:str originalURL:self baseURL:self.m3u_realBaseURL error:&err]; + if (err) { + completion(nil, err); + return; + } + + completion(listModel, nil); + }); +} + +@end diff --git a/ios/Video/RCTVideo-Bridging-Header.h b/ios/Video/RCTVideo-Bridging-Header.h index 6522d5ae53..1c69eeb7a8 100644 --- a/ios/Video/RCTVideo-Bridging-Header.h +++ b/ios/Video/RCTVideo-Bridging-Header.h @@ -1,3 +1,20 @@ +#import "CurrentVideos.h" +#import "M3U8ExtXByteRange.h" +#import "M3U8ExtXKey.h" +#import "M3U8ExtXMedia.h" +#import "M3U8ExtXMediaList.h" +#import "M3U8ExtXStreamInf.h" +#import "M3U8ExtXStreamInfList.h" +#import "M3U8LineReader.h" +#import "M3U8MasterPlaylist.h" +#import "M3U8MediaPlaylist.h" +#import "M3U8PlaylistModel.h" +#import "M3U8SegmentInfo.h" +#import "M3U8SegmentInfoList.h" +#import "M3U8TagsAndAttributes.h" +#import "NSArray+m3u8.h" +#import "NSString+m3u8.h" +#import "NSURL+m3u8.h" #import "RCTVideoSwiftLog.h" #import <React/RCTViewManager.h> diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 486524d947..8a3c9fbece 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -9,6 +9,14 @@ import React // MARK: - RCTVideo class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverHandler { + struct VideoState: OptionSet { + let rawValue: UInt + + static let unknown = VideoState(rawValue: 0) + static let loaded = VideoState(rawValue: 1 << 0) + static let ready = VideoState(rawValue: 1 << 1) + } + private var _player: AVPlayer? private var _playerItem: AVPlayerItem? private var _source: VideoSource? @@ -67,6 +75,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH private var _presentingViewController: UIViewController? private var _startPosition: Float64 = -1 private var _showNotificationControls = false + private var _principalVideo: NSNumber? + private var _peripheralVideo: NSNumber? + private var _videoState: VideoState = .unknown + private var _principalPendingPlayRequest = false private var _pictureInPictureEnabled = false { didSet { #if os(iOS) @@ -136,6 +148,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH @objc var onReceiveAdEvent: RCTDirectEventBlock? @objc var onTextTracks: RCTDirectEventBlock? @objc var onAudioTracks: RCTDirectEventBlock? + @objc var onVideoTracks: RCTDirectEventBlock? @objc var onTextTrackDataChanged: RCTDirectEventBlock? @objc @@ -268,6 +281,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _player?.pause() _player?.rate = 0.0 + RCTPlayerOperations.removeSpatialAudioRemoteCommandHandler() } @objc @@ -524,6 +538,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH let playerItem = try await self.preparePlayerItem() try await setupPlayer(playerItem: playerItem) + RCTPlayerOperations.addSpatialAudioRemoteCommandHandler() } catch { DebugLog("An error occurred: \(error.localizedDescription)") @@ -540,6 +555,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } self._videoLoadStarted = true + CurrentVideos.shared().add(self, forTag: self.reactTag) self.applyNextSource() } @@ -611,10 +627,86 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH playerItem.navigationMarkerGroups = RCTVideoTVUtils.makeNavigationMarkerGroups(chapters) } #endif - return playerItem } + func getAudioTrackInfo( + model _: M3U8PlaylistModel, + principalModel: M3U8PlaylistModel + ) -> [String: Any] { + var streamList: NSArray = .init() + + for i in 0 ..< principalModel.masterPlaylist.xStreamList.count { + let inf = principalModel.masterPlaylist.xStreamList.xStreamInf(at: i) + if let inf { + streamList.adding(inf) + } + } + + if let currentEvent: AVPlayerItemAccessLogEvent = _player?.currentItem?.accessLog()?.events.last { + let predicate = NSPredicate(format: "%K == %f", "bandwidth", currentEvent.indicatedBitrate) + let filteredArray = streamList.filtered(using: predicate) + let current = filteredArray.last + + if let current = current as? M3U8ExtXStreamInf { + let mediaList: NSArray = .init() + for i in 0 ..< principalModel.masterPlaylist.xMediaList.audio().count { + let inf = principalModel.masterPlaylist.xMediaList.audio().xMedia(at: i) + if let inf { + mediaList.adding(inf) + } + } + + let predicate = NSPredicate(format: "SELF.groupId == %@", current.audio) + let currentAudio = mediaList.filtered(using: predicate).last + + if let currentAudio = currentAudio as? M3U8ExtXMedia { + let url: URL = currentAudio.m3u8URL() + let audioModel: M3U8PlaylistModel? = try? .init(url: url) + + if let audioModel { + let audioInfo: M3U8SegmentInfo = audioModel.mainMediaPl.segmentList.segmentInfo(at: 0) + + return [ + "title": currentAudio.name(), + "language": currentAudio.language(), + "codecs": currentAudio.groupId(), + "file": audioInfo.uri.absoluteString, + "channels": currentAudio.channels, + ] + } + } + } + } + + return .init() + } + + func getVideoTrackInfo( + model: M3U8PlaylistModel, + principalModel: M3U8PlaylistModel + ) -> [String: Any] { + // swiftlint:disable:next empty_count + if !(model.mainMediaPl.segmentList.count == 0) { + let uri: URL = model.mainMediaPl.segmentList.segmentInfo(at: 0).uri + + var codecs = "" + // swiftlint:disable:next empty_count + if !(principalModel.masterPlaylist.xStreamList.count == 0) { + if let inf = principalModel.masterPlaylist.xStreamList.xStreamInf(at: 0) { + codecs = (inf.codecs as NSArray).componentsJoined(by: ",") + } + } + + let stringURL = uri.absoluteString as NSString + return [ + "file": stringURL, + "codecs": codecs, + ] + } + return .init() + } + // MARK: - Prop setters @objc @@ -710,6 +802,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH @objc func setPaused(_ paused: Bool) { + if self.isManaged() { + self.setManagedPaused(paused: paused) + return + } if paused { if _adPlaying { #if USE_GOOGLE_IMA @@ -742,6 +838,15 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH @objc func setSeek(_ info: NSDictionary!) { + if self.isManaged() { + if self.isPeripheral() { + return + } + let peripheral = self.peripheral() + self.setSeekManaged(info: info) + peripheral?.setSeekManaged(info: info) + return + } let seekTime: NSNumber! = info["time"] as! NSNumber let seekTolerance: NSNumber! = info["tolerance"] as! NSNumber let item: AVPlayerItem? = _player?.currentItem @@ -1123,6 +1228,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _playerLayer?.removeFromSuperlayer() _playerLayer = nil _playerObserver.playerLayer = nil + RCTPlayerOperations.removeSpatialAudioRemoteCommandHandler() } @objc @@ -1308,6 +1414,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH if _isBuffering { _isBuffering = false } + self.update(state: .ready) onReadyForDisplay?([ "target": reactTag, ]) @@ -1367,6 +1474,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } if onVideoLoad != nil, self._videoLoadStarted { + self.update(state: .loaded) var duration = Float(CMTimeGetSeconds(_playerItem.asset.duration)) if duration.isNaN || duration == 0 { @@ -1415,11 +1523,57 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } self._videoLoadStarted = false + self.handleMetadataUpdateForTrackChange() self._playerObserver.attachPlayerEventListeners() self.applyModifiers() } } + func handleMetadataUpdateForTrackChange() { + if onTextTracks != nil { + self.onTextTracks?( + [ + "textTrack": self._textTracks, + ] + ) + } + + guard let player = _player, + let urlString = _player?.currentItem?.accessLog()?.events.last?.uri, + let url = NSURL(string: urlString), + let principalURL: NSURL = (_player?.currentItem?.asset as? AVURLAsset)?.url as? NSURL else { + return + } + + principalURL.m3u_loadAsyncCompletion { principalModel, _ in + url.m3u_loadAsyncCompletion { model, _ in + if let model, let principalModel { + if self.onAudioTracks != nil { + self.onAudioTracks?( + [ + "audioTrack": self.getAudioTrackInfo( + model: model, + principalModel: principalModel + ), + ] + ) + } + + if self.onVideoTracks != nil { + self.onVideoTracks?( + [ + "videoTrack": self.getVideoTrackInfo( + model: model, + principalModel: principalModel + ), + ] + ) + } + } + } + } + } + func handlePlaybackFailed() { if let player = _player { NowPlayingInfoCenterManager.shared.removePlayer(player: player) @@ -1450,6 +1604,20 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // Continue playing (or not if paused) after being paused due to hitting an unbuffered zone. func handlePlaybackLikelyToKeepUp(playerItem _: AVPlayerItem, change _: NSKeyValueObservedChange<Bool>) { + if self.isManaged() { + if self.isPeripheral() { + self.onPeripheralVideoStatusChange() + } else { + _principalPendingPlayRequest = true + } + } else { + if (!_controls || _fullscreenPlayerPresented || _isBuffering) && _playerItem?.isPlaybackLikelyToKeepUp ?? false { + self.setPaused(_paused) + } + _isBuffering = false + self.onVideoBuffer?(["isBuffering": _isBuffering, "target": self.reactTag as Any]) + } + if _isBuffering { _isBuffering = false } @@ -1625,4 +1793,175 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // Workaround for #3418 - https://github.com/TheWidlarzGroup/react-native-video/issues/3418#issuecomment-2043508862 @objc func setOnClick(_: Any) {} + + func setPrincipalVideo(tag: NSNumber) { + _principalVideo = tag + } + + func setPeripheralVideo(tag: NSNumber) { + _peripheralVideo = tag + } + + func isPrincipal() -> Bool { + _peripheralVideo != nil + } + + func isPeripheral() -> Bool { + _principalVideo != nil + } + + func peripheral() -> RCTVideo? { + guard let video = _peripheralVideo else { + return nil + } + return CurrentVideos.shared().video(forTag: video) + } + + func principal() -> RCTVideo? { + guard let video = _principalVideo else { + return nil + } + return CurrentVideos.shared().video(forTag: video) + } + + func isManaged() -> Bool { + isPeripheral() || isPrincipal() + } + + func setManagedPaused(paused: Bool) { + if isPeripheral() { + return + } + guard let peripheral = peripheral() else { + return + } + var peripheralPlayer = peripheral._player + if paused { + _player?.pause() + peripheralPlayer?.pause() + _player?.rate = 0.0 + peripheralPlayer?.rate = 0.0 + } else { + if !isVideoReady() || peripheral.isVideoReady() { + _principalPendingPlayRequest = true + return + } + RCTPlayerOperations.configureAudio(ignoreSilentSwitch: _ignoreSilentSwitch, mixWithOthers: _mixWithOthers, audioOutput: _audioOutput) + + if #available(iOS 10.0, *), !_automaticallyWaitsToMinimizeStalling { + _player?.playImmediately(atRate: _rate) + peripheralPlayer?.playImmediately(atRate: _rate) + } else { + _player?.play() + peripheralPlayer?.play() + } + _player?.rate = _rate + peripheralPlayer?.rate = _rate + } + _paused = paused + peripheral.setRaw(paused: paused) + } + + func isVideoReady() -> Bool { + ((_videoState.rawValue & VideoState.loaded.rawValue) != 0) && ((_videoState.rawValue & VideoState.ready.rawValue) != 0) + } + + func setRaw(paused: Bool) { + _paused = paused + } + + func player() -> AVPlayer? { + _player + } + + func setSeekManaged(info: NSDictionary) { + if !isPrincipal() { + return + } + guard let peripheral = peripheral() else { + return + } + let seekTime: NSNumber? = info["time"] as? NSNumber + let seekTolerance: NSNumber? = info["tolerance"] as? NSNumber + + let timeScale: Int32 = 1000 + + let item: AVPlayerItem? = _player?.currentItem + let peripheralPlayer: AVPlayer? = peripheral.player() + let peripheralItem: AVPlayerItem? = peripheralPlayer?.currentItem + if let item, item.status == .readyToPlay, let peripheralItem, peripheralItem.status == .readyToPlay { + // TODO: check loadedTimeRanges + + let cmSeekTime: CMTime = CMTimeMakeWithSeconds(Float64(seekTime?.floatValue ?? .zero), preferredTimescale: timeScale) + let current: CMTime = item.currentTime() + let tolerance: CMTime = CMTimeMake(value: Int64(seekTolerance?.floatValue ?? .zero), timescale: timeScale) + + if CMTimeCompare(current, cmSeekTime) != 0 { + let wasPaused = _paused + _player?.pause() + peripheralPlayer?.pause() + + let seekGroup: DispatchGroup = .init() + seekGroup.enter() + _player?.seek(to: cmSeekTime, toleranceBefore: tolerance, toleranceAfter: tolerance, completionHandler: { _ in + seekGroup.leave() + }) + seekGroup.enter() + peripheralPlayer?.seek(to: cmSeekTime, toleranceBefore: tolerance, toleranceAfter: tolerance, completionHandler: { _ in + seekGroup.leave() + }) + + seekGroup.notify(queue: .main) { + self.seekCompletedFor(seekTime: seekTime ?? 0) + peripheral.seekCompletedFor(seekTime: seekTime ?? 0) + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.setManagedPaused(paused: wasPaused) + } + } + _pendingSeek = false + } else { + _pendingSeek = true + _pendingSeekTime = seekTime?.floatValue ?? .zero + } + } + } + + func seekCompletedFor(seekTime: NSNumber) { + let item: AVPlayerItem? = _player?.currentItem + _playerObserver.addTimeObserverIfNotSet() + if self.onVideoSeek != nil { + self.onVideoSeek?( + [ + "currentTime": NSNumber(value: CMTimeGetSeconds(item?.currentTime() ?? .zero)), + "seekTime": seekTime, + "target": self.reactTag, + ] + ) + } + } + + func update(state: VideoState) { + if !self.isManaged() { + return + } + _videoState != state + if self.isPeripheral() { + let principal = self.principal() + principal?.onPeripheralVideoStatusChange() + } + } + + func onPeripheralVideoStatusChange() { + let peripheral = self.peripheral() + if peripheral != nil { + return + } + if peripheral?.isVideoReady() ?? false && _principalPendingPlayRequest { + self.setPaused(false) + _principalPendingPlayRequest = false + } else if self.isVideoReady() && self.peripheral()?.isVideoReady() ?? false { + self.setPaused(false) + _principalPendingPlayRequest = false + } + } } diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index d0ead34a9c..e7e812903a 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -16,6 +16,8 @@ @interface RCT_EXTERN_MODULE (RCTVideoManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(selectedAudioTrack, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(chapters, NSArray); RCT_EXPORT_VIEW_PROPERTY(paused, BOOL); +RCT_EXPORT_VIEW_PROPERTY(principalVideo, NSNumber); +RCT_EXPORT_VIEW_PROPERTY(peripheralVideo, NSNumber); RCT_EXPORT_VIEW_PROPERTY(muted, BOOL); RCT_EXPORT_VIEW_PROPERTY(controls, BOOL); RCT_EXPORT_VIEW_PROPERTY(audioOutput, NSString); @@ -57,6 +59,7 @@ @interface RCT_EXTERN_MODULE (RCTVideoManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(onPlaybackStalled, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackResume, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackRateChange, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onPlayedTracksChange, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVolumeChange, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoPlaybackStateChanged, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoExternalPlaybackChange, RCTDirectEventBlock); @@ -66,6 +69,7 @@ @interface RCT_EXTERN_MODULE (RCTVideoManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(onReceiveAdEvent, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onTextTracks, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onAudioTracks, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onVideoTracks, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onTextTrackDataChanged, RCTDirectEventBlock); RCT_EXTERN_METHOD(save diff --git a/ios/Video/WeakVideoRef.h b/ios/Video/WeakVideoRef.h new file mode 100644 index 0000000000..ea9433d50a --- /dev/null +++ b/ios/Video/WeakVideoRef.h @@ -0,0 +1,17 @@ +// +// WeakVideoRef.h +// react-native-video +// +// Created by marcin.dziennik on 9/8/23. +// + +#import <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@class RCTVideo; +@interface WeakVideoRef : NSObject +@property(weak) RCTVideo* video; +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/Video/WeakVideoRef.m b/ios/Video/WeakVideoRef.m new file mode 100644 index 0000000000..0cb4506891 --- /dev/null +++ b/ios/Video/WeakVideoRef.m @@ -0,0 +1,12 @@ +// +// WeakVideoRef.m +// react-native-video +// +// Created by marcin.dziennik on 9/8/23. +// + +#import "WeakVideoRef.h" + +@implementation WeakVideoRef + +@end diff --git a/src/Video.tsx b/src/Video.tsx index e525799919..f0c4ee492b 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -66,6 +66,8 @@ export interface VideoRef { save: (options: object) => Promise<VideoSaveData>; setVolume: (volume: number) => void; getCurrentTime: () => Promise<number>; + setPrincipalVideoId: (principalId: number) => void; + setPeripheralVideoId: (peripheralId: number) => void; } const Video = forwardRef<VideoRef, ReactVideoProps>( @@ -78,6 +80,8 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( poster, fullscreen, drm, + principalVideo, + peripheralVideo, textTracks, selectedVideoTrack, selectedAudioTrack, @@ -118,6 +122,8 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( const nativeRef = useRef<ComponentRef<VideoComponentType>>(null); const [showPoster, setShowPoster] = useState(!!poster); const [isFullscreen, setIsFullscreen] = useState(fullscreen); + const [getPrincipalVideo, setPrincipalVideo] = useState(principalVideo); + const [getPeripheralVideo, setPeripheralVideo] = useState(peripheralVideo); const [ _restoreUserInterfaceForPIPStopCompletionHandler, setRestoreUserInterfaceForPIPStopCompletionHandler, @@ -300,6 +306,14 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( return VideoManager.setVolume(volume, getReactTag(nativeRef)); }, []); + const setPrincipalVideoId = useCallback((principalId: number) => { + setPrincipalVideo(principalId); + }, []); + + const setPeripheralVideoId = useCallback((peripheralId: number) => { + setPeripheralVideo(peripheralId); + }, []); + const onVideoLoadStart = useCallback( (e: NativeSyntheticEvent<OnLoadStartData>) => { hasPoster && setShowPoster(true); @@ -516,6 +530,8 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( pause, resume, getCurrentTime, + setPrincipalVideoId, + setPeripheralVideoId, restoreUserInterfaceForPictureInPictureStopCompleted, setVolume, }), @@ -527,6 +543,8 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( pause, resume, getCurrentTime, + setPrincipalVideoId, + setPeripheralVideoId, restoreUserInterfaceForPictureInPictureStopCompleted, setVolume, ], @@ -539,6 +557,8 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( {...rest} src={src} drm={_drm} + principalVideo={getPrincipalVideo} + peripheralVideo={getPeripheralVideo} style={StyleSheet.absoluteFill} resizeMode={resizeMode} fullscreen={isFullscreen} diff --git a/src/specs/VideoNativeComponent.ts b/src/specs/VideoNativeComponent.ts index 1f2672d69c..64a34536d2 100644 --- a/src/specs/VideoNativeComponent.ts +++ b/src/specs/VideoNativeComponent.ts @@ -342,6 +342,8 @@ export interface VideoNativeProps extends ViewProps { restoreUserInterfaceForPIPStopCompletionHandler?: boolean; localSourceEncryptionKeyScheme?: string; cookiePolicy: string; + principalVideo: Int32; + peripheralVideo: Int32; debug?: DebugConfig; showNotificationControls?: WithDefault<boolean, false>; // Android, iOS bufferConfig?: BufferConfig; // Android diff --git a/src/types/video.ts b/src/types/video.ts index 92ecf2ab95..e85054eadb 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -269,6 +269,8 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps { volume?: number; localSourceEncryptionKeyScheme?: string; cookiePolicy: EnumValues<CookiePolicy>; + principalVideo: number; + peripheralVideo: number; debug?: DebugConfig; allowsExternalPlayback?: boolean; // iOS controlsStyles?: ControlsStyles; // Android