一、MOV/MP4 视频文件中的 Rotation 元数据 iOS 上内置相机应用录制的 mov/mp4 视频可能产生一个 Rotation 元数据,表示录制视频时摄像头旋转到了多少角度。其值一般为这四个:0、90、180或270。类似于图片文件的 Exif
信息中的 Orientation 元数据。 Rotation 元数据用于播放器确定渲染视频的方向,但有的播放器会对其视而不见。稍后会测试几种常见的播放器/播放控件对 Rotation 元数据的支持。
注:实际上视频文件的 Rotation 元数据并不是保存的角度值,不过如果只关心角度问题而不是图像拉伸之类的,可以这样简单理解。关于如何获取 Rotation 元数据角度值,有兴趣的可以浏览 vlc 的源码。
下面用 MediaInfo
软件看看用 iPhone 相机应用使用后置摄像头录制的两个视频,观察其 Rotation 元数据。请留意文件名分别为 IMG_1427.MOV
和 IMG_1428.MOV
,后文也会用这两个文件做对比。
1、使用后置摄像头在 Portrait
(竖屏,Home 键在下边)模式时录制的视频,其 Rotation 值为90。
图1: Rotation 值为90
2、使用后置摄像头在 LandscapeRigth
(横屏,Home 键在右边)模式时录制的视频,则无 Rotation 元数据,或者说 Rotation 值为0。
图2: 无 Rotation 值或者说 Rotation 值为0
关于 Rotation 的0、90、180和270这四个角度值可以这样理解: LandscapeRigth
为0度;以Home键或摄像头为圆心,顺时针旋转到 Portrait
为90度;旋转到 LandscapeLeft
为180度;旋转到 PortraitUpsideDown
为270度。
这里先在 macOS 10.10.5 和 Windows 8 上看看这两个视频文件的属性: 1、将手机里的视频文件导出到 macOS ,并在 Finder 中看预览,两个文件的显示方向都是正确的。再查看 Rotation 值为90的 IMG_1427.MOV
视频文件的属性。显示其尺寸为 1080*1920
,而不是 1920*1080
。但不要被这个假象欺骗了,视频的实际尺寸还是 1920*1080
。最后看没有 Rotation 值或者说 Rotation 值为0的 IMG_1428.MOV
视频文件,显示其尺寸为 1920*1080
,一切正常。使用 QuickTime 播放,能正确识别出两个视频的方向。
图3
2、在 Windows 资源管理器中看预览, IMG_1427.MOV
和 IMG_1428.MOV
的显示方向都是正确的;再查看两个文件的属性,尺寸都显示为 1920*1080
;使用 Windows Media Player 播放,能正确识别出两个视频的方向。
二、常见视频播放器对方向的识别 iOS 相册调出的播放器和 Windows 8 上的 Windows Media Player 能够正确识别出 MOV/MP4 的方向,即实际尺寸为 1920*1080的
、Rotation 值为90的 IMG_1427.MOV
视频能够按 1080*1920
的尺寸并调整方向进行渲染;没有 Rotation 值或者说 Rotation 值为0的 IMG_1428.MOV
视频按 1920*1080
的尺寸并按实际方向进行渲染。Andriod 也存在类似情况。 VLC for macOS(why?)和 iOS 的 MPMoviePlayerViewControlle
对 Rotation 没有识别,它们总是按实际尺寸和默认方向进行渲染。对于 MPMoviePlayerViewControlle
下面有解决方案。 Safari 浏览器调出的播放器应该也是 MPMoviePlayerViewController
,所以也无法正确识别方向。
三、MPMoviePlayerViewController 控制视频方向 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //... NSString * url = @"http://www.yourdomain.com/Videos/1.m3u8"; MPMoviePlayerViewController * vc = [[MPMoviePlayerViewController alloc] init]; vc.moviePlayer.contentURL = [NSURL URLWithString:url]; // 这里播放一个Rotation为90的视频,即Home键在下录制的视频 [self rotateVideoView:vc degrees:90]; [self presentMoviePlayerViewControllerAnimated:vc]; [vc.moviePlayer play]; //... - (void)rotateVideoView:(MPMoviePlayerViewController *)movePlayerViewController degrees:(NSInteger)degrees { if(degrees==0||degrees==360) return; if(degrees<0) degrees = (degrees % 360) + 360; if(degrees>360) degrees = degrees % 360; // MPVideoView在iOS8中Tag为1002,不排除苹果以后更改的可能性。参考递归查看View层次结构的lldb命令: (lldb) po [movePlayerViewController.view recursiveDescription] UIView *videoView = [movePlayerViewController.view viewWithTag:1002]; if ([videoView isKindOfClass:NSClassFromString(@"MPVideoView")]) { videoView.transform = CGAffineTransformMakeRotation(M_PI * degrees / 180.0); videoView.frame = movePlayerViewController.view.bounds; } }
可将 rotateVideoView 加到 Category 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #import "MPMoviePlayerViewController+Rotation.h" @implementation MPMoviePlayerViewController (Rotation) - (void)rotateVideoViewWithDegrees:(NSInteger)degrees { if(degrees==0||degrees==360) return; if(degrees<0) degrees = (degrees % 360) + 360; if(degrees>360) degrees = degrees % 360; // MPVideoView在iOS8中Tag为1002,不排除苹果以后更改的可能性。参考递归查看View层次结构的lldb命令: (lldb) po [movePlayerViewController.view recursiveDescription] UIView *videoView = [self.view viewWithTag:1002]; if ([videoView isKindOfClass:NSClassFromString(@"MPVideoView")]) { videoView.transform = CGAffineTransformMakeRotation(M_PI * degrees / 180.0); videoView.frame = self.view.bounds; } } @end
四、HTML5 控制视频方向 在 video
标签中增加 style="-webkit-transform: rotate(90deg);”
,不过控件也被旋转了。这就需要将默认播放控件隐藏了并且自绘控件,此略。
五、使用 ffmpeg 写入 Rotation 元数据 对于没有 Rotation 元数据的 mp4 文件,可通过 ffmpeg
等工具写入。比如视频需要顺时针旋转90度显示:
1 ffmpeg -i input.mp4 -c copy -metadata:s:v:0 rotate=90 output.mp4
注:如果愿意,写入非0、90、180或270的值,比如45之类的也是可以的。
六、获取视频方向(角度) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 + (NSUInteger)degressFromVideoFileWithURL:(NSURL *)url { NSUInteger degress = 0; AVAsset *asset = [AVAsset assetWithURL:url]; NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo]; if([tracks count] > 0) { AVAssetTrack *videoTrack = [tracks objectAtIndex:0]; CGAffineTransform t = videoTrack.preferredTransform; if(t.a == 0 && t.b == 1.0 && t.c == -1.0 && t.d == 0) { // Portrait degress = 90; }else if(t.a == 0 && t.b == -1.0 && t.c == 1.0 && t.d == 0) { // PortraitUpsideDown degress = 270; }else if(t.a == 1.0 && t.b == 0 && t.c == 0 && t.d == 1.0){ // LandscapeRight degress = 0; }else if(t.a == -1.0 && t.b == 0 && t.c == 0 && t.d == -1.0) { // LandscapeLeft degress = 180; } } return degress; }
七、按正确方向对视频进行截图 关键点是将 AVAssetImageGrnerator
对象的 appliesPreferredTrackTransform
属性设置为 YES 。
八、实时视频的方向处理 使用 AVFoundation
制作自定义相机时,采集出来的视频帧保存在 CMSampleBufferRef
结构中,颜色空间可以设置为 sRGB 或 YUV。进行一些内存操作就可实现旋转。以下代码是针对 YUV 的。
注:这种涉及大量内存拷贝的操作,实际应用中要权衡其利弊。以下代码未经过测试。
1、RGB24 旋转90度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void RGB24Rotate90 (int8_t *des, const int8_t *src, int width, int height) { if (!des || !src) return ; int n = 0 ; int linesize = width * 3 ; int i, j; for (j = width; j > 0 ; j--) { for (i = 0 ; i < height; i++) { memccpy(&des[n], &src[linesize * i + j * 3 - 3 ], 0 , 3 ); n += 3 ; } } }
2、YUV 旋转90度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void YUV420Rotate90 (int8_t *des, const int8_t *src, int width, int height) { int i = 0 , j = 0 , n = 0 ; int hw = width / 2 , hh = height / 2 ; const int8_t *ptmp = src; for (j = width; j > 0 ; j--) { for (i = 0 ; i < height; i++) { des[n++] = ptmp[width * i + j]; } } ptmp = src + width * height; for (j = hw; j > 0 ; j--) { for (i = 0 ; i < hh; i++) { des[n++] = ptmp[hw * i + j]; } } ptmp = src + width * height * 5 / 4 ; for (j = hw; j > 0 ; j--) { for (i = 0 ; i < hh; i++) { des[n++] = ptmp[hw * i + j]; } } }
或:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 int8_t [] rotateYUV420Degree90(int8_t [] data, int imageWidth, int imageHeight){ int8_t [] yuv = new int8_t [imageWidth*imageHeight*3 /2 ]; int i = 0 ; for (int x = 0 ;x < imageWidth;x++) { for (int y = imageHeight-1 ;y >= 0 ;y--) { yuv[i] = data[y*imageWidth+x]; i++; } } i = imageWidth*imageHeight*3 /2 -1 ; for (int x = imageWidth-1 ;x > 0 ;x=x-2 ) { for (int y = 0 ;y < imageHeight/2 ;y++) { yuv[i] = data[(imageWidth*imageHeight)+(y*imageWidth)+x]; i--; yuv[i] = data[(imageWidth*imageHeight)+(y*imageWidth)+(x-1 )]; i--; } } return yuv; }
参考资料
环境:
macOS 10.10.5
Xcode 6.4(6E35b)
iOS >= 7.0