mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
474 lines
16 KiB
Objective-C
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
|