From 014a509836afad7e2e3c7e0b677befbcec3ef782 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Fri, 16 Sep 2022 11:35:55 -0700 Subject: [PATCH 1/8] [video_player]fix a blank screen bug for protected video stream in ios 16 --- .../video_player_avfoundation/CHANGELOG.md | 3 +- .../ios/RunnerTests/VideoPlayerTests.m | 43 +++++++++++++++++++ .../ios/Classes/FLTVideoPlayerPlugin.m | 26 +++++++++++ .../video_player_avfoundation/pubspec.yaml | 2 +- 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index 3c9cc2d371fd..cc4411c8c651 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.3.6 +* Fixes a bug in iOS 16 where videos from protected live streams are not shown. * Updates minimum Flutter version to 2.10. * Fixes violations of new analysis option use_named_constants. * Fixes avoid_redundant_argument_values lint warnings and minor typos. diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m index 7decd04bd168..813fca2b8e7d 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m @@ -11,6 +11,10 @@ @interface FLTVideoPlayer : NSObject @property(readonly, nonatomic) AVPlayer *player; +// This is to fix a bug (https://github.com/flutter/flutter/issues/111457) in iOS 16 with blank +// video for encrypted video streams. An invisible AVPlayerLayer is used to overwrite the +// protection of pixel buffers in those streams. +@property(readonly, nonatomic) AVPlayerLayer *playerLayer; @end @interface FLTVideoPlayerPlugin (Test) @@ -61,6 +65,45 @@ @interface VideoPlayerTests : XCTestCase @implementation VideoPlayerTests +- (void)testIOS16BugWithEncryptedVideoStream { + // This is to fix a bug (https://github.com/flutter/flutter/issues/111457) in iOS 16 with blank + // video for encrypted video streams. An invisible AVPlayerLayer is used to overwrite the + // protection of pixel buffers in those streams. + // Note that a better unit test is to validate that `copyPixelBuffer` API returns the pixel + // buffers as expected, which requires setting up the video player properly with a protected video + // stream (.m3u8 file). + NSObject *registry = + (NSObject *)[[UIApplication sharedApplication] delegate]; + NSObject *registrar = + [registry registrarForPlugin:@"testPlayerLayerWorkaround"]; + FLTVideoPlayerPlugin *videoPlayerPlugin = + [[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar]; + + FlutterError *error; + [videoPlayerPlugin initialize:&error]; + XCTAssertNil(error); + + FLTCreateMessage *create = [FLTCreateMessage + makeWithAsset:nil + uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" + packageName:nil + formatHint:nil + httpHeaders:@{}]; + FLTTextureMessage *textureMessage = [videoPlayerPlugin create:create error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(textureMessage); + FLTVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureMessage.textureId]; + XCTAssertNotNil(player); + + if (@available(iOS 16.0, *)) { + XCTAssertNotNil(player.playerLayer, @"AVPlayerLayer should be present for iOS 16."); + XCTAssertNotNil(player.playerLayer.superlayer, + @"AVPlayerLayer should be added on screen for iOS 16."); + } else { + XCTAssertNil(player.playerLayer, @"AVPlayerLayer should not be present before iOS 16."); + } +} + - (void)testSeekToInvokesTextureFrameAvailableOnTextureRegistry { NSObject *mockTextureRegistry = OCMProtocolMock(@protocol(FlutterTextureRegistry)); diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m index a95779b1cbab..645c86d6eade 100644 --- a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m +++ b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m @@ -36,6 +36,8 @@ - (void)onDisplayLink:(CADisplayLink *)link { @interface FLTVideoPlayer : NSObject @property(readonly, nonatomic) AVPlayer *player; @property(readonly, nonatomic) AVPlayerItemVideoOutput *videoOutput; +/// An invisible player layer used to access the pixel buffers in protected video streams in iOS 16. +@property(readonly, nonatomic) AVPlayerLayer *playerLayer; @property(readonly, nonatomic) CADisplayLink *displayLink; @property(nonatomic) FlutterEventChannel *eventChannel; @property(nonatomic) FlutterEventSink eventSink; @@ -132,6 +134,19 @@ NS_INLINE CGFloat radiansToDegrees(CGFloat radians) { return degrees; }; +NS_INLINE UIViewController *rootViewController() API_AVAILABLE(ios(16.0)) { + for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) { + if ([scene isKindOfClass:UIWindowScene.class]) { + for (UIWindow *window in ((UIWindowScene *)scene).windows) { + if (window.isKeyWindow) { + return window.rootViewController; + } + } + } + } + return nil; +} + - (AVMutableVideoComposition *)getVideoCompositionWithTransform:(CGAffineTransform)transform withAsset:(AVAsset *)asset withVideoTrack:(AVAssetTrack *)videoTrack { @@ -227,6 +242,14 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item _player = [AVPlayer playerWithPlayerItem:item]; _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; + // This is to fix a bug (https://github.com/flutter/flutter/issues/111457) in iOS 16 with blank + // video for encrypted video streams. An invisible AVPlayerLayer is used to overwrite the + // protection of pixel buffers in those streams. + if (@available(iOS 16.0, *)) { + _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; + [rootViewController().view.layer addSublayer:_playerLayer]; + } + [self createVideoOutputAndDisplayLink:frameUpdater]; [self addObservers:item]; @@ -458,6 +481,9 @@ - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments /// so the channel is going to die or is already dead. - (void)disposeSansEventChannel { _disposed = YES; + if (@available(iOS 16.0, *)) { + [_playerLayer removeFromSuperlayer]; + } [_displayLink invalidate]; AVPlayerItem *currentItem = self.player.currentItem; [currentItem removeObserver:self forKeyPath:@"status"]; diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 06042c34bad6..bd88ddf94876 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_avfoundation description: iOS implementation of the video_player plugin. repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.3.5 +version: 2.3.6 environment: sdk: ">=2.14.0 <3.0.0" From 6f704bd7316093fed023635ab6acbf1c7c507370 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Tue, 20 Sep 2022 23:29:40 -0700 Subject: [PATCH 2/8] added integration tests for video player --- .../ios/RunnerUITests/VideoPlayerUITests.m | 32 ++++++++- .../example/lib/main.dart | 66 ++++++++++++++++++- 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m index b9f0f16bb27b..47def8cbbfa8 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m @@ -46,7 +46,7 @@ - (void)testPlayVideo { XCTAssertTrue(foundPlaybackSpeed5x); // Cycle through tabs. - for (NSString *tabName in @[ @"Asset", @"Remote" ]) { + for (NSString *tabName in @[ @"Asset mp4", @"Remote mp4" ]) { NSPredicate *predicate = [NSPredicate predicateWithFormat:@"label BEGINSWITH %@", tabName]; XCUIElement *unselectedTab = [app.staticTexts elementMatchingPredicate:predicate]; XCTAssertTrue([unselectedTab waitForExistenceWithTimeout:30.0]); @@ -60,4 +60,34 @@ - (void)testPlayVideo { } } +- (void)testEncryptedVideoStream { + // This is to fix a bug (https://github.com/flutter/flutter/issues/111457) in iOS 16 with blank + // video for encrypted video streams. + + NSString *tabName = @"Remote enc m3u8"; + + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"label BEGINSWITH %@", tabName]; + XCUIElement *unselectedTab = [self.app.staticTexts elementMatchingPredicate:predicate]; + XCTAssertTrue([unselectedTab waitForExistenceWithTimeout:30.0]); + XCTAssertFalse(unselectedTab.isSelected); + [unselectedTab tap]; + + XCUIElement *selectedTab = [self.app.otherElements + elementMatchingPredicate:[NSPredicate predicateWithFormat:@"label BEGINSWITH %@", tabName]]; + XCTAssertTrue([selectedTab waitForExistenceWithTimeout:30.0]); + XCTAssertTrue(selectedTab.isSelected); + + NSMutableSet *frames = [NSMutableSet set]; + int numberOfFrames = 5; + for (int i = 0; i < numberOfFrames; i++) { + NSLog(@"Snapshotting frame %d", i); + UIImage *image = self.app.screenshot.image; + [frames addObject:UIImagePNGRepresentation(image)]; + [NSThread sleepForTimeInterval:1]; + } + + // At least 1 loading and 2 distinct frames (3 in total) to validate that the video is playing. + XCTAssert(frames.count >= 3, @"Must have at least 3 distinct frames."); +} + @end diff --git a/packages/video_player/video_player_avfoundation/example/lib/main.dart b/packages/video_player/video_player_avfoundation/example/lib/main.dart index bca4e291efff..121fb6e680f0 100644 --- a/packages/video_player/video_player_avfoundation/example/lib/main.dart +++ b/packages/video_player/video_player_avfoundation/example/lib/main.dart @@ -20,7 +20,7 @@ class _App extends StatelessWidget { @override Widget build(BuildContext context) { return DefaultTabController( - length: 2, + length: 3, child: Scaffold( key: const ValueKey('home_page'), appBar: AppBar( @@ -30,15 +30,20 @@ class _App extends StatelessWidget { tabs: [ Tab( icon: Icon(Icons.cloud), - text: 'Remote', + text: 'Remote mp4', ), - Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset'), + Tab( + icon: Icon(Icons.favorite), + text: 'Remote enc m3u8', + ), + Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset mp4'), ], ), ), body: TabBarView( children: [ _BumbleBeeRemoteVideo(), + _BumbleBeeEncryptedLiveStream(), _ButterFlyAssetVideo(), ], ), @@ -156,6 +161,61 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { } } +class _BumbleBeeEncryptedLiveStream extends StatefulWidget { + @override + _BumbleBeeEncryptedLiveStreamState createState() => + _BumbleBeeEncryptedLiveStreamState(); +} + +class _BumbleBeeEncryptedLiveStreamState + extends State<_BumbleBeeEncryptedLiveStream> { + late MiniController _controller; + + @override + void initState() { + super.initState(); + _controller = MiniController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/encrypted_bee.m3u8', + ); + + _controller.addListener(() { + setState(() {}); + }); + _controller.initialize(); + + _controller.play(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Container(padding: const EdgeInsets.only(top: 20.0)), + const Text('With remote encrypted m3u8'), + Container( + padding: const EdgeInsets.all(20), + child: _controller.value.isInitialized + ? AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + ) + : Container( + child: const Text('loading...'), + ), + ), + ], + ), + ); + } +} + class _ControlsOverlay extends StatelessWidget { const _ControlsOverlay({Key? key, required this.controller}) : super(key: key); From 0306914a17a7d6a342b1e80bc2a8a0e184f2e55c Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Thu, 22 Sep 2022 11:31:24 -0700 Subject: [PATCH 3/8] add a delay to avoid network timeout --- .../example/ios/RunnerUITests/VideoPlayerUITests.m | 4 +++- .../video_player_avfoundation/example/lib/main.dart | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m index 47def8cbbfa8..8705fb563e72 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m @@ -77,10 +77,12 @@ - (void)testEncryptedVideoStream { XCTAssertTrue([selectedTab waitForExistenceWithTimeout:30.0]); XCTAssertTrue(selectedTab.isSelected); + // Wait until the video is loaded. + [NSThread sleepForTimeInterval:60]; + NSMutableSet *frames = [NSMutableSet set]; int numberOfFrames = 5; for (int i = 0; i < numberOfFrames; i++) { - NSLog(@"Snapshotting frame %d", i); UIImage *image = self.app.screenshot.image; [frames addObject:UIImagePNGRepresentation(image)]; [NSThread sleepForTimeInterval:1]; diff --git a/packages/video_player/video_player_avfoundation/example/lib/main.dart b/packages/video_player/video_player_avfoundation/example/lib/main.dart index 121fb6e680f0..d385fd0ee66a 100644 --- a/packages/video_player/video_player_avfoundation/example/lib/main.dart +++ b/packages/video_player/video_player_avfoundation/example/lib/main.dart @@ -206,9 +206,7 @@ class _BumbleBeeEncryptedLiveStreamState aspectRatio: _controller.value.aspectRatio, child: VideoPlayer(_controller), ) - : Container( - child: const Text('loading...'), - ), + : const Text('loading...'), ), ], ), From 9114c3b331904eb43b179f890070c477f59ceaa4 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Thu, 22 Sep 2022 11:44:09 -0700 Subject: [PATCH 4/8] try 60 frames --- .../example/ios/RunnerUITests/VideoPlayerUITests.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m index 8705fb563e72..e568e5deb2cb 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m @@ -81,7 +81,7 @@ - (void)testEncryptedVideoStream { [NSThread sleepForTimeInterval:60]; NSMutableSet *frames = [NSMutableSet set]; - int numberOfFrames = 5; + int numberOfFrames = 60; for (int i = 0; i < numberOfFrames; i++) { UIImage *image = self.app.screenshot.image; [frames addObject:UIImagePNGRepresentation(image)]; From 758ba46b6bbea40b553f913a24237f57f4e89d1d Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Thu, 22 Sep 2022 11:49:45 -0700 Subject: [PATCH 5/8] take random itnerval of 1-2 seconds instead of a fixed interval of 1 second --- .../example/ios/RunnerUITests/VideoPlayerUITests.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m index e568e5deb2cb..205e45f5ebe5 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m @@ -85,7 +85,10 @@ - (void)testEncryptedVideoStream { for (int i = 0; i < numberOfFrames; i++) { UIImage *image = self.app.screenshot.image; [frames addObject:UIImagePNGRepresentation(image)]; - [NSThread sleepForTimeInterval:1]; + // Take random interval between [1, 2) seconds, since the video length could be the same as a + // fixed interval, which would always result in the same frame. + NSTimeInterval sampleInterval = 1 + ((double)arc4random() / UINT32_MAX); + [NSThread sleepForTimeInterval:sampleInterval]; } // At least 1 loading and 2 distinct frames (3 in total) to validate that the video is playing. From 4d4d534f00fd65f7fb845d6b58cf02525253252b Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Thu, 22 Sep 2022 12:05:27 -0700 Subject: [PATCH 6/8] add screenshot attachment for debugging if the test fails in the future --- .../example/ios/RunnerUITests/VideoPlayerUITests.m | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m index 205e45f5ebe5..26b345e6b52f 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m @@ -83,7 +83,14 @@ - (void)testEncryptedVideoStream { NSMutableSet *frames = [NSMutableSet set]; int numberOfFrames = 60; for (int i = 0; i < numberOfFrames; i++) { - UIImage *image = self.app.screenshot.image; + XCUIScreenshot *screenshot = self.app.screenshot; + + // Attach the screenshots for debugging if the test fails. + XCTAttachment *attachment = [XCTAttachment attachmentWithScreenshot:screenshot]; + attachment.lifetime = XCTAttachmentLifetimeKeepAlways; + [self addAttachment:attachment]; + + UIImage *image = screenshot.image; [frames addObject:UIImagePNGRepresentation(image)]; // Take random interval between [1, 2) seconds, since the video length could be the same as a // fixed interval, which would always result in the same frame. From 52a27f466ad00dc54e576af3013a291e8eacb70a Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Thu, 22 Sep 2022 15:35:55 -0700 Subject: [PATCH 7/8] remove random sample interval --- .../example/ios/RunnerUITests/VideoPlayerUITests.m | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m index 26b345e6b52f..efb61f359610 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m @@ -92,10 +92,9 @@ - (void)testEncryptedVideoStream { UIImage *image = screenshot.image; [frames addObject:UIImagePNGRepresentation(image)]; - // Take random interval between [1, 2) seconds, since the video length could be the same as a - // fixed interval, which would always result in the same frame. - NSTimeInterval sampleInterval = 1 + ((double)arc4random() / UINT32_MAX); - [NSThread sleepForTimeInterval:sampleInterval]; + // The sample interval must NOT be the same as video length. + // Otherwise it would always result in the same frame. + [NSThread sleepForTimeInterval:1]; } // At least 1 loading and 2 distinct frames (3 in total) to validate that the video is playing. From 13380274cdb5745f9b0d09206081ff0a3ddd21ba Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Thu, 22 Sep 2022 17:14:29 -0700 Subject: [PATCH 8/8] print out base 64 string instead of attach the image --- .../ios/RunnerUITests/VideoPlayerUITests.m | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m index efb61f359610..531d4fdf213c 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m @@ -4,6 +4,7 @@ @import os.log; @import XCTest; +@import CoreGraphics; @interface VideoPlayerUITests : XCTestCase @property(nonatomic, strong) XCUIApplication *app; @@ -83,15 +84,24 @@ - (void)testEncryptedVideoStream { NSMutableSet *frames = [NSMutableSet set]; int numberOfFrames = 60; for (int i = 0; i < numberOfFrames; i++) { - XCUIScreenshot *screenshot = self.app.screenshot; + UIImage *image = self.app.screenshot.image; - // Attach the screenshots for debugging if the test fails. - XCTAttachment *attachment = [XCTAttachment attachmentWithScreenshot:screenshot]; - attachment.lifetime = XCTAttachmentLifetimeKeepAlways; - [self addAttachment:attachment]; + // Plugin CI does not support attaching screenshot. + // Convert the image to base64 encoded string, and print it out for debugging purpose. + // NSLog truncates long strings, so need to scale downn image. + CGSize smallerSize = CGSizeMake(100, 200); + UIGraphicsBeginImageContextWithOptions(smallerSize, NO, 0.0); + [image drawInRect:CGRectMake(0, 0, smallerSize.width, smallerSize.height)]; + UIImage *smallerImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + // 0.5 compression is good enough for debugging purpose. + NSData *imageData = UIImageJPEGRepresentation(smallerImage, 0.5); + NSString *imageString = [imageData base64EncodedStringWithOptions:0]; + NSLog(@"frame %d image data:\n%@", i, imageString); + + [frames addObject:imageString]; - UIImage *image = screenshot.image; - [frames addObject:UIImagePNGRepresentation(image)]; // The sample interval must NOT be the same as video length. // Otherwise it would always result in the same frame. [NSThread sleepForTimeInterval:1];