Shaka Player Embedded
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
ShakaPlayerView.mm
Go to the documentation of this file.
1 // Copyright 2020 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
16 
17 #include <unordered_set>
18 
19 #include "shaka/utils.h"
21 
22 
23 @interface ShakaPlayerView () {
24  CADisplayLink *_renderDisplayLink;
25  NSTimer *_textLoopTimer;
26  CALayer *_imageLayer;
27  CALayer *_textLayer;
28  CALayer *_avPlayerLayer;
31 
32  NSMutableDictionary<NSValue *, NSSet<CALayer *> *> *_cues;
33 }
34 
35 - (void)renderLoop;
36 
37 @end
38 
43 @interface LoopWrapper : NSObject
44 @end
45 
46 @implementation LoopWrapper {
47  __weak ShakaPlayerView *_target;
48 }
49 
50 - (id)initWithTarget:(ShakaPlayerView *)target {
51  if ((self = [super init])) {
52  _target = target;
53  }
54  return self;
55 }
56 
57 - (void)renderLoop:(CADisplayLink *)sender {
58  [self->_target renderLoop];
59 }
60 
61 @end
62 
63 @implementation ShakaPlayerView
64 
65 // MARK: setup
66 
67 - (instancetype)initWithFrame:(CGRect)frame {
68  if ((self = [super initWithFrame:frame])) {
69  [self setup];
70  }
71  return self;
72 }
73 
74 - (instancetype)initWithCoder:(NSCoder *)coder {
75  if ((self = [super initWithCoder:coder])) {
76  [self setup];
77  }
78  return self;
79 }
80 
81 - (instancetype)initWithPlayer:(ShakaPlayer *)player {
82  if ((self = [super init])) {
83  [self setup];
84  [self setPlayer:player];
85  }
86  return self;
87 }
88 
89 - (void)dealloc {
90  [_renderDisplayLink invalidate];
91  [_textLoopTimer invalidate];
92 }
93 
94 - (void)setup {
95  _renderDisplayLink =
96  [CADisplayLink displayLinkWithTarget:[[LoopWrapper alloc] initWithTarget:self]
97  selector:@selector(renderLoop:)];
98  [_renderDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
99 
100  // Use a block with a capture to a weak variable to ensure that the
101  // view will be destroyed once there are no other references.
102  __weak ShakaPlayerView *weakSelf = self;
103  _textLoopTimer = [NSTimer timerWithTimeInterval:0.25
104  repeats:YES
105  block:^(NSTimer *timer) {
106  [weakSelf textLoop];
107  }];
108  [[NSRunLoop mainRunLoop] addTimer:_textLoopTimer forMode:NSRunLoopCommonModes];
109 
110 
111  // Set up the image layer.
112  _imageLayer = [CALayer layer];
113  [self.layer addSublayer:_imageLayer];
114 
115  // Set up the text layer.
116  _textLayer = [CALayer layer];
117  [self.layer addSublayer:_textLayer];
118 
119  // Disable default animations.
120  _imageLayer.actions = @{@"position": [NSNull null], @"bounds": [NSNull null]};
121 
123  _cues = [[NSMutableDictionary alloc] init];
124 }
125 
126 - (ShakaPlayer *)player {
127  return _player;
128 }
129 
130 - (void)setPlayer:(ShakaPlayer *)player {
131  if (_avPlayerLayer) {
132  [_avPlayerLayer removeFromSuperlayer];
133  _avPlayerLayer = nil;
134  }
135 
136  _player = player;
137  if (_player) {
138  _player.mediaPlayer->SetVideoFillMode(_gravity);
139 
140  _avPlayerLayer = static_cast<CALayer *>(CFBridgingRelease(player.mediaPlayer->GetIosView()));
141  [self.layer addSublayer:_avPlayerLayer];
142  }
143 }
144 
145 - (void)setVideoGravity:(AVLayerVideoGravity)videoGravity {
146  if (videoGravity == AVLayerVideoGravityResize) {
148  } else if (videoGravity == AVLayerVideoGravityResizeAspectFill) {
149  _gravity = shaka::VideoFillMode::Zoom;
150  } else if (videoGravity == AVLayerVideoGravityResizeAspect) {
152  } else {
153  [NSException raise:NSGenericException format:@"Invalid value for videoGravity"];
154  }
155  if (_player)
156  _player.mediaPlayer->SetVideoFillMode(_gravity);
157 }
158 
159 // MARK: rendering
160 
161 - (void)renderLoop {
162  if (!_player) {
163  self->_imageLayer.contents = nil;
164  return;
165  }
166 
167  shaka::Rational<uint32_t> aspect_ratio;
168  if (CGImageRef image = _player.videoRenderer->Render(nullptr, &aspect_ratio)) {
169  _imageLayer.contents = (__bridge_transfer id)image;
170 
171  // Fit image in frame.
172  shaka::ShakaRect<uint32_t> image_bounds = {0, 0, CGImageGetWidth(image),
173  CGImageGetHeight(image)};
174  shaka::ShakaRect<uint32_t> dest_bounds = {
175  0,
176  0,
177  static_cast<uint32_t>(self.bounds.size.width),
178  static_cast<uint32_t>(self.bounds.size.height),
179  };
182  shaka::FitVideoToRegion(image_bounds, dest_bounds, aspect_ratio,
183  _player.videoRenderer->fill_mode(), &src, &dest);
184  _imageLayer.contentsRect = CGRectMake(
185  static_cast<CGFloat>(src.x) / image_bounds.w, static_cast<CGFloat>(src.y) / image_bounds.h,
186  static_cast<CGFloat>(src.w) / image_bounds.w, static_cast<CGFloat>(src.h) / image_bounds.h);
187  _imageLayer.frame = CGRectMake(dest.x, dest.y, dest.w, dest.h);
188  }
189 }
190 
191 - (void)layoutSubviews {
192  [super layoutSubviews];
193  if (_avPlayerLayer)
194  _avPlayerLayer.frame = self.bounds;
195 }
196 
197 - (void)textLoop {
198  BOOL sizeChanged = _textLayer.frame.size.width != _imageLayer.frame.size.width ||
199  _textLayer.frame.size.height != _imageLayer.frame.size.height;
200 
201  _textLayer.frame = _imageLayer.frame;
202  _textLayer.hidden = ![self remakeTextCues:sizeChanged];
203 }
204 
205 - (BOOL)remakeTextCues:(BOOL)sizeChanged {
206  if (!_player)
207  return NO;
208 
209  auto text_tracks = _player.mediaPlayer->TextTracks();
210  if (text_tracks.empty())
211  return NO;
212  auto activeCues = text_tracks[0]->active_cues(_player.currentTime);
213  if (text_tracks[0]->mode() != shaka::media::TextTrackMode::Showing) {
214  return NO;
215  }
216 
217  if (sizeChanged) {
218  for (CALayer *layer in [_textLayer.sublayers copy])
219  [layer removeFromSuperlayer];
220  [_cues removeAllObjects];
221  }
222 
223  // Use the system font for body. The ensures that if the user changes their font size, it will use
224  // that font size.
225  UIFont *font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
226 
227  // Add layers for new cues.
228  for (unsigned long i = 0; i < activeCues.size(); i++) {
229  // Read the cues in inverse order, so the oldest cue is at the bottom.
230  std::shared_ptr<shaka::media::VTTCue> cue = activeCues[activeCues.size() - i - 1];
231  if (_cues[[NSValue valueWithPointer:cue.get()]] != nil)
232  continue;
233 
234  NSString *cueText = [NSString stringWithUTF8String:cue->text().c_str()];
235  NSMutableSet<CALayer *> *layers = [[NSMutableSet alloc] init];
236  for (NSString *line in [cueText componentsSeparatedByString:@"\n"]) {
237  CATextLayer *cueLayer = [[CATextLayer alloc] init];
238  cueLayer.string = line;
239  cueLayer.font = CGFontCreateWithFontName(static_cast<CFStringRef>(font.fontName));
240  cueLayer.fontSize = font.pointSize;
241  cueLayer.backgroundColor = [UIColor blackColor].CGColor;
242  cueLayer.foregroundColor = [UIColor whiteColor].CGColor;
243  cueLayer.alignmentMode = kCAAlignmentCenter;
244 
245  // TODO: Take into account direction setting, snap to lines, position align, etc.
246 
247  // Determine size of cue line.
248  CGFloat width = static_cast<CGFloat>(cue->size() * (_textLayer.bounds.size.width) * 0.01);
249  NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin;
250  // TODO: Sometimes, if the system font size is set high, this will make very wrong estimates.
251  CGSize effectiveSize = [line boundingRectWithSize:CGSizeMake(width, 9999)
252  options:options
253  attributes:@{NSFontAttributeName: font}
254  context:nil]
255  .size;
256  // The size estimates don't always seem to be 100% accurate, so this adds a bit to the width.
257  width = ceil(effectiveSize.width) + 16;
258  CGFloat height = ceil(effectiveSize.height);
259  CGFloat x = (_textLayer.bounds.size.width - width) * 0.5f;
260  cueLayer.frame = CGRectMake(x, 0, width, height);
261 
262  [_textLayer addSublayer:cueLayer];
263  [layers addObject:cueLayer];
264  }
265  [_cues setObject:layers forKey:[NSValue valueWithPointer:cue.get()]];
266  }
267 
268  // Remove any existing cues that aren't active anymore.
269  std::unordered_set<shaka::media::VTTCue *> activeCuesSet;
270  for (auto &cue : activeCues)
271  activeCuesSet.insert(cue.get());
272  NSMutableSet<CALayer *> *expectedLayers = [[NSMutableSet alloc] init];
273  for (NSValue *cue in [_cues allKeys]) {
274  if (activeCuesSet.count(reinterpret_cast<shaka::media::VTTCue *>([cue pointerValue])) == 0) {
275  // Since the layers aren't added to "expectedLayers", they will be removed below.
276  [_cues removeObjectForKey:cue];
277  } else {
278  NSSet<CALayer *> *layers = _cues[cue];
279  [expectedLayers unionSet:layers];
280  }
281  }
282 
283  // Remove any layers in the view that aren't associated with any cues.
284  for (CALayer *layer in [_textLayer.sublayers copy]) {
285  if (![expectedLayers containsObject:layer])
286  [layer removeFromSuperlayer];
287  }
288 
289  // Move all the remaining layers to appear at the bottom of the screen.
290  CGFloat y = _textLayer.bounds.size.height;
291  for (auto i = _textLayer.sublayers.count; i > 0; i--) {
292  // Read the cue layers in inverse order, since they are being drawn from the bottom up.
293  CALayer *cueLayer = _textLayer.sublayers[i - 1];
294 
295  CGSize size = cueLayer.frame.size;
296  y -= size.height;
297  cueLayer.frame = CGRectMake(cueLayer.frame.origin.x, y, size.width, size.height);
298  }
299  return YES;
300 }
301 
302 @end
const char * dest
Definition: media_utils.cc:31
bool SetVideoFillMode(VideoFillMode mode) override
VideoFillMode
Definition: utils.h:41
shaka::media::DefaultMediaPlayer * mediaPlayer
NSTimer * _textLoopTimer
int width
ShakaPlayer * _player
CALayer * _avPlayerLayer
NSMutableDictionary< NSValue *, NSSet< CALayer * > * > * _cues
ShakaPlayer * player
int height
shaka::VideoFillMode _gravity
CADisplayLink * _renderDisplayLink
void FitVideoToRegion(ShakaRect< uint32_t > frame, ShakaRect< uint32_t > bounds, Rational< uint32_t > sample_aspect_ratio, VideoFillMode mode, ShakaRect< uint32_t > *src, ShakaRect< uint32_t > *dest)
Definition: shaka_utils.cc:23