Swiftgram/submodules/LegacyComponents/Sources/TGEmbedYoutubePlayerView.m
2020-02-22 15:38:54 +04:00

474 lines
16 KiB
Objective-C

#import "TGEmbedYoutubePlayerView.h"
#import "TGEmbedPlayerState.h"
#import "LegacyComponentsInternal.h"
NSString *const TGYTPlayerCallbackOnReady = @"onReady";
NSString *const TGYTPlayerCallbackOnState = @"onState";
NSString *const TGYTPlayerCallbackOnPlaybackQualityChange = @"onPlaybackQualityChange";
NSString *const TGYTPlayerCallbackOnError = @"onError";
const NSInteger TGYTPlayerStateUnstartedCode = -1;
const NSInteger TGYTPlayerStateEndedCode = 0;
const NSInteger TGYTPlayerStatePlayingCode = 1;
const NSInteger TGYTPlayerStatePausedCode = 2;
const NSInteger TGYTPlayerStateBufferingCode = 3;
@interface TGEmbedYoutubePlayerView ()
{
NSDictionary *_playerParams;
bool _started;
bool _failed;
bool _ready;
bool _playOnReady;
NSInteger _playAfterTicks;
NSInteger _ignorePositionUpdates;
}
@end
@implementation TGEmbedYoutubePlayerView
- (instancetype)initWithWebPageAttachment:(TGWebPageMediaAttachment *)webPage thumbnailSignal:(SSignal *)thumbnailSignal alternateCachePathSignal:(SSignal *)alternateCachePathSignal
{
self = [super initWithWebPageAttachment:webPage thumbnailSignal:thumbnailSignal alternateCachePathSignal:alternateCachePathSignal];
if (self != nil)
{
NSTimeInterval start = 0.0;
NSString *videoId = [TGEmbedYoutubePlayerView _youtubeVideoIdFromText:webPage.embedUrl originalUrl:webPage.url startTime:&start];
_playerParams = @
{
@"videoId": videoId,
@"playerVars": @
{
@"cc_load_policy" : @1,
@"iv_load_policy" : @3,
@"controls" : @0,
@"playsinline" : @1,
@"autohide" : @1,
@"showinfo" : @0,
@"rel" : @0,
@"modestbranding" : @1,
@"start" : @((NSInteger)start)
}
};
self.controlsView.watermarkImage = TGComponentsImageNamed(@"YoutubeWatermark");
self.controlsView.watermarkPosition = TGEmbedPlayerWatermarkPositionBottomRight;
self.controlsView.watermarkOffset = CGPointMake(-12.0f, -12.0f);
}
return self;
}
- (void)_watermarkAction
{
[super _watermarkAction];
if (self.onWatermarkAction != nil)
self.onWatermarkAction();
NSString *videoId = _playerParams[@"videoId"];
NSURL *appUrl = [[NSURL alloc] initWithString:[[NSString alloc] initWithFormat:@"youtube://watch?v=%@", videoId]];
if ([[LegacyComponentsGlobals provider] canOpenURL:appUrl])
{
[[LegacyComponentsGlobals provider] openURL:appUrl];
return;
}
NSURL *webUrl = [NSURL URLWithString:[NSString stringWithFormat:@"https://youtube.com/watch?v=%@", videoId]];
[[LegacyComponentsGlobals provider] openURL:webUrl];
}
- (void)playVideo
{
if (!_ready && self.disallowAutoplay)
{
_playOnReady = true;
_playAfterTicks = 2;
return;
}
[super playVideo];
[self _evaluateJS:@"player.playVideo();" completion:nil];
_ignorePositionUpdates = 2;
}
- (void)pauseVideo:(bool)manually
{
[super pauseVideo:manually];
[self _evaluateJS:@"player.pauseVideo();" completion:nil];
}
- (void)seekToPosition:(NSTimeInterval)position
{
NSString *command = [NSString stringWithFormat:@"player.seekTo(%@, true);", @(position)];
[self _evaluateJS:command completion:nil];
TGEmbedPlayerState *newState = [TGEmbedPlayerState stateWithPlaying:self.state.isPlaying duration:self.state.duration position:position downloadProgress:self.state.downloadProgress buffering:self.state.buffering];
[self updateState:newState];
_ignorePositionUpdates = 2;
}
- (TGEmbedPlayerControlsType)_controlsType
{
return TGEmbedPlayerControlsTypeFull;
}
- (void)_onPageReady
{
}
- (void)_didBeginPlayback
{
[super _didBeginPlayback];
[self setDimmed:false animated:true shouldDelay:false];
}
- (void)_notifyOfCallbackURL:(NSURL *)url
{
NSString *action = url.host;
NSString *query = url.query;
NSString *data;
if (query != nil)
data = [query componentsSeparatedByString:@"="][1];
if ([action isEqualToString:TGYTPlayerCallbackOnState])
{
if (_failed)
return;
NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:false];
NSArray *queryItems = urlComponents.queryItems;
bool failed = _failed;
bool playing = self.state.playing;
bool finished = false;
NSTimeInterval position = self.state.position;
NSTimeInterval duration = self.state.duration;
CGFloat downloadProgress = self.state.downloadProgress;
bool buffering = self.state.buffering;
for (NSURLQueryItem *queryItem in queryItems)
{
if ([queryItem.name isEqualToString:@"playback"])
{
playing = ([queryItem.value integerValue] == TGYTPlayerStatePlayingCode);
finished = ([queryItem.value integerValue] == TGYTPlayerStateEndedCode);
buffering = ([queryItem.value integerValue] == TGYTPlayerStateBufferingCode);
}
else if ([queryItem.name isEqualToString:@"position"])
{
if (_ignorePositionUpdates > 0)
_ignorePositionUpdates--;
else
position = [queryItem.value doubleValue];
}
else if ([queryItem.name isEqualToString:@"duration"])
{
duration = [queryItem.value doubleValue];
}
else if ([queryItem.name isEqualToString:@"download"])
{
downloadProgress = MAX(0.0f, MIN(1.0, [queryItem.value floatValue]));
}
else if ([queryItem.name isEqualToString:@"failed"])
{
failed = [queryItem.value boolValue];
}
}
if (failed && !_failed)
{
_failed = true;
[self setDimmed:false animated:true shouldDelay:false];
[self.controlsView setDisabled];
}
if (playing && !_started)
{
_started = true;
[self _didBeginPlayback];
}
if (finished)
position = 0.0;
TGEmbedPlayerState *newState = [TGEmbedPlayerState stateWithPlaying:playing duration:duration position:position downloadProgress:downloadProgress buffering:buffering];
[self updateState:newState];
if (_playAfterTicks > 0)
{
_playAfterTicks--;
if (_playAfterTicks == 0)
{
_ready = true;
[self playVideo];
}
}
}
else if ([action isEqualToString:TGYTPlayerCallbackOnReady])
{
_ready = true;
if (_playOnReady)
{
_playAfterTicks = 0;
_playOnReady = false;
[self playVideo];
}
if (!self.disallowAutoplay)
{
dispatch_async(dispatch_get_main_queue(), ^
{
[self playVideo];
TGDispatchAfter(2.0, dispatch_get_main_queue(), ^{
if (!_started)
[self playVideo];
});
});
}
}
}
- (NSString *)_embedHTML
{
NSDictionary *playerCallbacks = @
{
@"onReady" : @"onReady",
@"onStateChange" : @"onStateChange",
@"onPlaybackQualityChange" : @"onPlaybackQualityChange",
@"onError" : @"onPlayerError"
};
NSMutableDictionary *playerParams = [[NSMutableDictionary alloc] init];
[playerParams addEntriesFromDictionary:_playerParams];
if (![playerParams objectForKey:@"height"])
[playerParams setValue:@"100%" forKey:@"height"];
if (![playerParams objectForKey:@"width"])
[playerParams setValue:@"100%" forKey:@"width"];
[playerParams setValue:playerCallbacks forKey:@"events"];
if ([playerParams objectForKey:@"playerVars"])
{
NSMutableDictionary *playerVars = [[NSMutableDictionary alloc] init];
[playerVars addEntriesFromDictionary:[playerParams objectForKey:@"playerVars"]];
}
else
{
[playerParams setValue:[[NSDictionary alloc] init] forKey:@"playerVars"];
}
NSError *error = nil;
NSString *path = TGComponentsPathForResource(@"YoutubePlayer", @"html");
NSString *embedHTMLTemplate = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
if (error != nil)
{
TGLegacyLog(@"[YTEmbedPlayer]: Received error rendering template: %@", error);
return nil;
}
NSError *jsonRenderingError = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:playerParams options:NSJSONWritingPrettyPrinted error:&jsonRenderingError];
if (jsonRenderingError != nil)
{
NSLog(@"[YTEmbedPlayer]: Attempted configuration of player with invalid playerVars: %@ \tError: %@", playerParams, jsonRenderingError);
return nil;
}
NSString *playerVarsJsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
NSString *autoplay = self.disallowAutoplay ? @"false" : @"true";
NSString *embedHTML = [NSString stringWithFormat:embedHTMLTemplate, playerVarsJsonString, autoplay];
return embedHTML;
}
- (NSURL *)_baseURL
{
return [NSURL URLWithString:@"https://youtube.com/"];
}
- (void)_setupUserScripts:(WKUserContentController *)contentController
{
NSError *error = nil;
NSString *path = TGComponentsPathForResource(@"YoutubePlayerInject", @"js");
NSString *scriptText = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
if (error != nil)
TGLegacyLog(@"[YTEmbedPlayer]: Received error loading inject script: %@", error);
WKUserScript *script = [[WKUserScript alloc] initWithSource:scriptText injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:false];
[contentController addUserScript:script];
}
- (bool)_scaleViewToMaxSize
{
return true;
}
- (CGFloat)_compensationEdges
{
return 3.0f;
}
+ (NSString *)_youtubeVideoIdFromText:(NSString *)text originalUrl:(NSString *)originalUrl startTime:(NSTimeInterval *)startTime
{
if ([text hasPrefix:@"http://www.youtube.com/watch?v="] || [text hasPrefix:@"https://www.youtube.com/watch?v="] || [text hasPrefix:@"http://m.youtube.com/watch?v="] || [text hasPrefix:@"https://m.youtube.com/watch?v="])
{
NSRange range1 = [text rangeOfString:@"?v="];
bool match = true;
for (NSInteger i = range1.location + range1.length; i < (NSInteger)text.length; i++)
{
unichar c = [text characterAtIndex:i];
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '=' || c == '&' || c == '#'))
{
match = false;
break;
}
}
if (match)
{
NSString *videoId = nil;
NSRange ampRange = [text rangeOfString:@"&"];
NSRange hashRange = [text rangeOfString:@"#"];
if (ampRange.location != NSNotFound || hashRange.location != NSNotFound)
{
NSInteger location = MIN(ampRange.location, hashRange.location);
videoId = [text substringWithRange:NSMakeRange(range1.location + range1.length, location - range1.location - range1.length)];
}
else
videoId = [text substringFromIndex:range1.location + range1.length];
if (videoId.length != 0)
return videoId;
}
}
else if ([text hasPrefix:@"http://youtu.be/"] || [text hasPrefix:@"https://youtu.be/"] || [text hasPrefix:@"http://www.youtube.com/embed/"] || [text hasPrefix:@"https://www.youtube.com/embed/"])
{
NSString *suffix = @"";
NSMutableArray *prefixes = [NSMutableArray arrayWithArray:@
[
@"http://youtu.be/",
@"https://youtu.be/",
@"http://www.youtube.com/embed/",
@"https://www.youtube.com/embed/"
]];
while (suffix.length == 0 && prefixes.count > 0)
{
NSString *prefix = prefixes.firstObject;
if ([text hasPrefix:prefix])
{
suffix = [text substringFromIndex:prefix.length];
break;
}
else
{
[prefixes removeObjectAtIndex:0];
}
}
NSString *queryString = nil;
for (int i = 0; i < (int)suffix.length; i++)
{
unichar c = [suffix characterAtIndex:i];
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '=' || c == '&' || c == '#'))
{
if (c == '?')
{
queryString = [suffix substringFromIndex:i + 1];
suffix = [suffix substringToIndex:i];
break;
}
else
{
return nil;
}
}
}
if (startTime != NULL)
{
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
NSString *queryString = [NSURL URLWithString:originalUrl].query;
for (NSString *param in [queryString componentsSeparatedByString:@"&"])
{
NSArray *components = [param componentsSeparatedByString:@"="];
if (components.count < 2)
continue;
[params setObject:components.lastObject forKey:components.firstObject];
}
NSString *timeParam = params[@"t"];
if (timeParam == nil)
timeParam = params[@"time_continue"];
if (timeParam != nil)
{
NSTimeInterval position = 0.0;
if ([timeParam rangeOfString:@"s"].location != NSNotFound)
{
NSString *value;
NSUInteger location = 0;
for (NSUInteger i = 0; i < timeParam.length; i++)
{
unichar c = [timeParam characterAtIndex:i];
if ((c < '0' || c > '9'))
{
value = [timeParam substringWithRange:NSMakeRange(location, i - location)];
location = i + 1;
switch (c)
{
case 's':
position += value.doubleValue;
break;
case 'm':
position += value.doubleValue * 60.0;
break;
case 'h':
position += value.doubleValue * 3600.0;
break;
default:
break;
}
}
}
}
else
{
position = timeParam.doubleValue;
}
*startTime = position;
}
}
return suffix;
}
return nil;
}
+ (bool)_supportsWebPage:(TGWebPageMediaAttachment *)webPage
{
NSString *url = webPage.embedUrl;
if ([url rangeOfString:@"list"].location != NSNotFound)
return false;
return ([url hasPrefix:@"http://www.youtube.com/watch?v="] || [url hasPrefix:@"https://www.youtube.com/watch?v="] || [url hasPrefix:@"http://m.youtube.com/watch?v="] || [url hasPrefix:@"https://m.youtube.com/watch?v="] || [url hasPrefix:@"http://youtu.be/"] || [url hasPrefix:@"https://youtu.be/"] || [url hasPrefix:@"https://www.youtube.com/embed/"]);
}
@end