版本:
OS X 10.10.5
Xcode 6.4(6E35b)
iOS >= 7
一、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。

(圖一:Rotation值為90)
2、使用后置攝像頭在LandscapeRigth(橫屏,Home鍵在右邊)模式時錄制的視頻,則無Rotation元數據,或者說Rotation值為0。

(圖二:無Rotation值或者說Rotation值為0)
關於Rotation的0、90、180和270這四個角度值可以這樣理解:LandscapeRigth為0度;以Home鍵或攝像頭為圓心,順時針旋轉到Portrait為90度;旋轉到LandscapeLeft為180度;旋轉到PortraitUpsideDown為270度。
這里先在OS X 10.10.4和Windows 8上看看這兩個視頻文件的屬性:
1、將手機里的視頻文件導出到OS X,並在Finder中看預覽,兩個文件的顯示方向都是正確的。再查看Rotation值為90的IMG_1427.MOV視頻文件的屬性。顯示其尺寸為1080*1920,而不是1920*1080。但不要被這個假象欺騙了,視頻的實際尺寸還是1920*1080。最后看沒有Rotation值或者說Rotation值為0的IMG_1428.MOV視頻文件,顯示其尺寸為1920*1080,一切正常。使用QuickTime播放,能正確識別出兩個視頻的方向。

(圖三)
2、在Windows資源管理器中看預覽,IMG_1427.MOV和IMG_1428.MOV的顯示方向都是正確的;再查看兩個文件的屬性,尺寸都顯示為1920*1080;使用Windows Media Player播放,能正確識別出兩個視頻的方向。
二、常見視頻播放器對方向的識別
iOS相冊調出的播放器和Win8上的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 OS X(why?)和iOS的MPMoviePlayerViewControlle對Rotation沒有識別,它們總是按實際尺寸和默認方向進行渲染。對於MPMoviePlayerViewControlle下面有解決方案。
Safari瀏覽器調出的播放器應該也是MPMoviePlayerViewController,所以也無法正確識別方向。
三、MPMoviePlayerViewController控制視頻方向
需要額外的參數來確定視頻的方向,然后旋轉播放器,達到各種視頻——mov/mp4/m3u8等——都可以正確播放的目的。

1 //…... 2 NSString * url = @"http://www.yourdomain.com/Videos/1.m3u8"; 3 MPMoviePlayerViewController * vc = [[MPMoviePlayerViewController alloc] init]; 4 vc.moviePlayer.contentURL = [NSURL URLWithString:url]; 5 // 這里播放一個Rotation為90的視頻,即Home鍵在下錄制的視頻 6 [self rotateVideoView:vc degrees:90]; 7 [self presentMoviePlayerViewControllerAnimated:vc]; 8 [vc.moviePlayer play]; 9 10 //…... 11 - (void)rotateVideoView:(MPMoviePlayerViewController *)movePlayerViewController degrees:(NSInteger)degrees 12 { 13 if(degrees==0||degrees==360) return; 14 if(degrees<0) degrees = (degrees % 360) + 360; 15 if(degrees>360) degrees = degrees % 360; 16 // MPVideoView在iOS8中Tag為1002,不排除蘋果以后更改的可能性。參考遞歸查看View層次結構的lldb命令: (lldb) po [movePlayerViewController.view recursiveDescription] 17 UIView *videoView = [movePlayerViewController.view viewWithTag:1002]; 18 if ([videoView isKindOfClass:NSClassFromString(@"MPVideoView")]) { 19 videoView.transform = CGAffineTransformMakeRotation(M_PI * degrees / 180.0); 20 videoView.frame = movePlayerViewController.view.bounds; 21 } 22 }
改為Category:
1 #import "MPMoviePlayerViewController+Rotation.h" 2 3 @implementation MPMoviePlayerViewController (Rotation) 4 5 - (void)rotateVideoViewWithDegrees:(NSInteger)degrees 6 { 7 if(degrees==0||degrees==360) return; 8 if(degrees<0) degrees = (degrees % 360) + 360; 9 if(degrees>360) degrees = degrees % 360; 10 11 // MPVideoView在iOS8中Tag為1002,不排除蘋果以后更改的可能性。參考遞歸查看View層次結構的lldb命令: (lldb) po [movePlayerViewController.view recursiveDescription] 12 UIView *videoView = [self.view viewWithTag:1002]; 13 if ([videoView isKindOfClass:NSClassFromString(@"MPVideoView")]) { 14 videoView.transform = CGAffineTransformMakeRotation(M_PI * degrees / 180.0); 15 videoView.frame = self.view.bounds; 16 } 17 } 18 19 @end
四、HTML5控制視頻方向
在video標簽中增加
style="-webkit-transform: rotate(90deg);” ,不過控件也被旋轉了。這就需要將默認播放控件隱藏了並且自繪控件,此略。
五、使用ffmpeg寫入Rotation元數據
對於沒有Rotation元數據的mp4文件,可通過ffmpeg等工具寫入。比如視頻需要順時針旋轉90度顯示:
ffmpeg -i input.mp4 -c copy -metadata:s:v:0 rotate=90 output.mp4
注:如果願意,寫入非0、90、180或270的值,比如45之類的也是可以的。
六、獲取視頻方向(角度)
+ (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。
1 + (UIImage *)extractImageFromVideoFileWithUrl:(NSURL *)url 2 { 3 NSDictionary *opts = @{AVURLAssetPreferPreciseDurationAndTimingKey:@(NO)}; 4 AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:opts]; 5 AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:asset]; 6 // 應用方向 7 gen.appliesPreferredTrackTransform = YES; 8 CMTime time = CMTimeMakeWithSeconds(1, 60); 9 NSError *error = nil; 10 CMTime actualTime; 11 CGImageRef image = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error]; 12 if(error) 13 { 14 DLog(@"%@ %@",__FUNCTION_FILE_LINE__,error); 15 return nil; 16 } 17 UIImage *thumb = [[UIImage alloc] initWithCGImage:image]; 18 CGImageRelease(image); 19 20 return thumb; 21 }
八、實時視頻的方向處理
使用AVFoundation制作自定義相機時,采集出來的視頻幀保存在CMSampleBufferRef結構中,顏色空間可以設置為sRGB或YUV。進行一些內存操作就可實現旋轉。以下代碼是針對YUV的。
注:這種涉及大量內存拷貝的操作,實際應用中要權衡其利弊。以下代碼未經過測試。
1、RGB24旋轉90度

1 // RGB24旋轉90度 2 void RGB24Rotate90(int8_t *des, const int8_t *src, int width, int height) 3 { 4 if(!des || !src) return; 5 6 int n = 0; 7 int linesize = width * 3; 8 int i, j; 9 // 逆時針旋轉 10 for (j = width; j > 0; j--) { 11 for (i = 0; i < height; i++) { 12 memccpy(&des[n], &src[linesize * i + j * 3 - 3], 0, 3); 13 n += 3; 14 } 15 } 16 /* 17 // 順時針旋轉 18 for (j = 0 ; j < width; j++) { 19 for (i = height; i > 0; i--) { 20 memccpy(&des[n], &src[linesize * (i - 1) + j * 3 - 3], 0, 3); 21 n += 3; 22 } 23 } 24 */ 25 }
2、RGB24旋轉90度

1 // YUV420旋轉90度 2 void YUV420Rotate90(int8_t *des, const int8_t *src, int width, int height) 3 { 4 int i = 0, j = 0, n = 0; 5 int hw = width / 2, hh = height / 2; 6 7 const int8_t *ptmp = src; 8 for (j = width; j > 0; j--) { 9 for (i = 0; i < height; i++) { 10 des[n++] = ptmp[width * i + j]; 11 } 12 } 13 14 ptmp = src + width * height; 15 for (j = hw; j > 0; j--) { 16 for (i = 0; i < hh; i++) { 17 des[n++] = ptmp[hw * i + j]; 18 } 19 } 20 21 ptmp = src + width * height * 5 / 4; 22 for (j = hw; j > 0; j--) { 23 for (i = 0; i < hh; i++) { 24 des[n++] = ptmp[hw * i + j]; 25 } 26 } 27 }
或:

1 int8_t[] rotateYUV420Degree90(int8_t[] data, int imageWidth, int imageHeight) 2 { 3 int8_t [] yuv = new int8_t[imageWidth*imageHeight*3/2]; 4 // Rotate the Y luma 5 int i = 0; 6 for(int x = 0;x < imageWidth;x++) 7 { 8 for(int y = imageHeight-1;y >= 0;y--) 9 { 10 yuv[i] = data[y*imageWidth+x]; 11 i++; 12 } 13 } 14 // Rotate the U and V color components 15 i = imageWidth*imageHeight*3/2-1; 16 for(int x = imageWidth-1;x > 0;x=x-2) 17 { 18 for(int y = 0;y < imageHeight/2;y++) 19 { 20 yuv[i] = data[(imageWidth*imageHeight)+(y*imageWidth)+x]; 21 i--; 22 yuv[i] = data[(imageWidth*imageHeight)+(y*imageWidth)+(x-1)]; 23 i--; 24 } 25 } 26 return yuv; 27 }