Alby's blog

世上没有巧合,只有巧合的假象。

0%

关于视频方向的若干问题

一、MOV/MP4 视频文件中的 Rotation 元数据

iOS 上内置相机应用录制的 mov/mp4 视频可能产生一个 Rotation 元数据,表示录制视频时摄像头旋转到了多少角度。其值一般为这四个:0、90、180或270。类似于图片文件的 Exif 信息中的 Orientation 元数据。
Rotation 元数据用于播放器确定渲染视频的方向,但有的播放器会对其视而不见。稍后会测试几种常见的播放器/播放控件对 Rotation 元数据的支持。

注:实际上视频文件的 Rotation 元数据并不是保存的角度值,不过如果只关心角度问题而不是图像拉伸之类的,可以这样简单理解。关于如何获取 Rotation 元数据角度值,有兴趣的可以浏览 vlc 的源码。

下面用 MediaInfo 软件看看用 iPhone 相机应用使用后置摄像头录制的两个视频,观察其 Rotation 元数据。请留意文件名分别为 IMG_1427.MOVIMG_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.MOVIMG_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;
}
}
/*
// 顺时针旋转
for (j = 0 ; j < width; j++) {
for (i = height; i > 0; i--) {
memccpy(&des[n], &src[linesize * (i - 1) + 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
// YUV420 旋转90度
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];
// Rotate the Y luma
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++;
}
}
// Rotate the U and V color components
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