我正在使用 AVFoundation 將兩個視頻合并為一個。我嘗試的結果是單個視頻的長度等于所有剪輯的總和,并顯示黑屏。
這是我的代碼:
public void mergeclips()
{
AVMutableComposition mixComposition = new AVMutableComposition();
CMTime previous_asset_duration = CMTime.Zero;
CMTime AllAssetDurations = CMTime.Zero;
AVMutableVideoCompositionLayerInstruction[] Instruction_Array = new AVMutableVideoCompositionLayerInstruction[Clips.Count];
foreach (string clip in Clips)
{
#region HoldVideoTrack
AVAsset asset = AVAsset.FromUrl(NSUrl.FromFilename(clip));
AVMutableCompositionTrack Track = mixComposition.AddMutableTrack(AVMediaType.Video, 0);
CMTimeRange range = new CMTimeRange()
{
Start = new CMTime(0, 0),
Duration = asset.Duration
};
AVAssetTrack track = asset.TracksWithMediaType(AVMediaType.Video)[0];
Track.InsertTimeRange(range, track, previous_asset_duration, out NSError error);
#endregion
#region Instructions
// 7
var Instruction = AVMutableVideoCompositionLayerInstruction.FromAssetTrack(Track);
Instruction.SetOpacity(0, asset.Duration);
// 8
Instruction_Array[Clips.IndexOf(clip)] = Instruction;
#endregion
previous_asset_duration = asset.Duration;
AllAssetDurations = asset.Duration;
}
// 6
var mainInstruction = new List<AVMutableVideoCompositionInstruction>();
CMTimeRange rangeIns = new CMTimeRange()
{
Start = new CMTime(0, 0),
Duration = AllAssetDurations
};
mainInstruction[0].TimeRange = rangeIns;
mainInstruction[0].LayerInstructions = Instruction_Array;
var mainComposition = new AVMutableVideoComposition();
mainComposition.Instructions = mainInstruction.ToArray();
mainComposition.FrameDuration = new CMTime(1, 30);
mainComposition.RenderSize = new CoreGraphics.CGSize(UIScreen.MainScreen.Bounds.Width, UIScreen.MainScreen.Bounds.Height);
//... export video ...
AVAssetExportSession exportSession = new AVAssetExportSession(mixComposition, AVAssetExportSessionPreset.MediumQuality)
{
OutputUrl = NSUrl.FromFilename(Path.Combine(Path.GetTempPath(), "temporaryClip/Whole.mov")),
OutputFileType = AVFileType.QuickTimeMovie,
ShouldOptimizeForNetworkUse = true,
//APP crashes here
VideoComposition = mainComposition
};
exportSession.ExportAsynchronously(_OnExportDone);
}
private static void _OnExportDone()
{
var library = new ALAssetsLibrary();
library.WriteVideoToSavedPhotosAlbum(NSUrl.FromFilename(Path.Combine(Path.GetTempPath(), "temporaryClip/Whole.mov")), (path, e2) =>
{
if (e2 != null)
{
new UIAlertView("Error", e2.ToString(), null, "OK", null).Show();
}
else
{
}
});
}
編輯: 我添加了更多代碼,具體來說,我向 AVAssetExportSession 添加了“ShouldOptimizeForNetworkUse”和 VideoCompositions。我使用 List 而不是 AVMutableVideoCompositionInstruction 因為 AVMutableVideoComposition.Instructions 需要一個型別為 AVVideoCompositionInstructions[] 的類。使用前面的代碼,應用程式在以下行“VideoComposition = mainComposition”崩潰
編輯:在包含對指令的轉換并進行 Shawn 指出的更正后,我可以合并 2 個或更多視頻并將常見視頻保存到一個檔案中。不幸的是,根本問題仍然存在,最終視頻僅顯示 AVMutableVideoCompositionInstruction 的 backgroundColor,而不是我們預期的所有剪輯。這些視頻的音頻也被忽略,我不知道這是否必須分開添加,但知道它可能也有幫助。
這是我的代碼:
public void mergeclips()
{
AVMutableComposition mixComposition = new AVMutableComposition();
AVMutableVideoCompositionLayerInstruction[] Instruction_Array = new AVMutableVideoCompositionLayerInstruction[Clips.Count];
foreach (string clip in Clips)
{
#region HoldVideoTrack
AVAsset asset = AVAsset.FromUrl(NSUrl.FromFilename(clip));
AVMutableCompositionTrack Track = mixComposition.AddMutableTrack(AVMediaType.Video, 0);
CMTimeRange range = new CMTimeRange()
{
Start = new CMTime(0, 0),
Duration = asset.Duration
};
AVAssetTrack track = asset.TracksWithMediaType(AVMediaType.Video)[0];
Track.InsertTimeRange(range, track, mixComposition.Duration, out NSError error);
#endregion
#region Instructions
Instruction_Array[Clips.IndexOf(clip)] = SetInstruction(asset, mixComposition.Duration, Track);
#endregion
}
// 6
var mainInstruction = new AVMutableVideoCompositionInstruction();
CMTimeRange rangeIns = new CMTimeRange()
{
Start = new CMTime(0, 0),
Duration = mixComposition.Duration
};
mainInstruction.BackgroundColor = UIColor.FromRGBA(1f, 1f, 1f, 1.000f).CGColor;
mainInstruction.TimeRange = rangeIns;
mainInstruction.LayerInstructions = Instruction_Array;
var mainComposition = new AVMutableVideoComposition()
{
Instructions = new AVVideoCompositionInstruction[1] { mainInstruction },
FrameDuration = new CMTime(1, 30),
RenderSize = new CoreGraphics.CGSize(UIScreen.MainScreen.Bounds.Width, UIScreen.MainScreen.Bounds.Height)
};
//... export video ...
AVAssetExportSession exportSession = new AVAssetExportSession(mixComposition, AVAssetExportSessionPreset.MediumQuality)
{
OutputUrl = NSUrl.FromFilename(Path.Combine(Path.GetTempPath(), "temporaryClip/Whole.mov")),
OutputFileType = AVFileType.QuickTimeMovie,
ShouldOptimizeForNetworkUse = true,
VideoComposition = mainComposition
};
exportSession.ExportAsynchronously(_OnExportDone);
}
private AVMutableVideoCompositionLayerInstruction SetInstruction(AVAsset asset, CMTime currentTime, AVMutableCompositionTrack assetTrack)
{
var instruction = AVMutableVideoCompositionLayerInstruction.FromAssetTrack(assetTrack);
var transform = assetTrack.PreferredTransform;
var transformSize = assetTrack.NaturalSize; //for export session
var newAssetSize = new CoreGraphics.CGSize(transformSize.Width, transformSize.Height); // for export session
if (newAssetSize.Width > newAssetSize.Height)//portrait
{
//Starting here, all newassetsize have its height and width inverted, height should be width and vice versa
var scaleRatio = UIScreen.MainScreen.Bounds.Height / newAssetSize.Width;
var _coreGraphic = new CoreGraphics.CGAffineTransform(0, 0, 0, 0, 0, 0);
_coreGraphic.Scale(scaleRatio, scaleRatio);
var tx = UIScreen.MainScreen.Bounds.Width / 2 - newAssetSize.Height * scaleRatio / 2;
var ty = UIScreen.MainScreen.Bounds.Height / 2 - newAssetSize.Width * scaleRatio / 2;
_coreGraphic.Translate(tx, ty);
instruction.SetTransform(_coreGraphic, currentTime);
}
var endTime = CMTime.Add(currentTime, asset.Duration);
instruction.SetOpacity(0, endTime);
return instruction;
}
編輯:多虧了 Shawn 的幫助,代碼中的幾個錯誤得到了糾正。問題依然存在(生成的視頻沒有影像)
這是我的代碼:
public void mergeclips()
{
//microphone
AVCaptureDevice microphone = AVCaptureDevice.DefaultDeviceWithMediaType(AVMediaType.Audio);
AVMutableComposition mixComposition = new AVMutableComposition();
AVMutableVideoCompositionLayerInstruction[] Instruction_Array = new AVMutableVideoCompositionLayerInstruction[Clips.Count];
foreach (string clip in Clips)
{
#region HoldVideoTrack
AVAsset asset = AVAsset.FromUrl(NSUrl.FromFilename(clip));
CMTimeRange range = new CMTimeRange()
{
Start = new CMTime(0, 0),
Duration = asset.Duration
};
AVMutableCompositionTrack videoTrack = mixComposition.AddMutableTrack(AVMediaType.Video, 0);
AVAssetTrack assetVideoTrack = asset.TracksWithMediaType(AVMediaType.Video)[0];
videoTrack.InsertTimeRange(range, assetVideoTrack, mixComposition.Duration, out NSError error);
if (microphone != null)
{
AVMutableCompositionTrack audioTrack = mixComposition.AddMutableTrack(AVMediaType.Audio, 0);
AVAssetTrack assetAudioTrack = asset.TracksWithMediaType(AVMediaType.Audio)[0];
audioTrack.InsertTimeRange(range, assetAudioTrack, mixComposition.Duration, out NSError error2);
}
#endregion
#region Instructions
Instruction_Array[Clips.IndexOf(clip)] = SetInstruction(asset, mixComposition.Duration, videoTrack);
#endregion
}
// 6
var mainInstruction = new AVMutableVideoCompositionInstruction();
CMTimeRange rangeIns = new CMTimeRange()
{
Start = new CMTime(0, 0),
Duration = mixComposition.Duration
};
mainInstruction.BackgroundColor = UIColor.FromRGBA(1f, 1f, 1f, 1.000f).CGColor;
mainInstruction.TimeRange = rangeIns;
mainInstruction.LayerInstructions = Instruction_Array;
var mainComposition = new AVMutableVideoComposition()
{
Instructions = new AVVideoCompositionInstruction[1] { mainInstruction },
FrameDuration = new CMTime(1, 30),
RenderSize = new CoreGraphics.CGSize(UIScreen.MainScreen.Bounds.Width, UIScreen.MainScreen.Bounds.Height)
};
//... export video ...
AVAssetExportSession exportSession = new AVAssetExportSession(mixComposition, AVAssetExportSessionPreset.MediumQuality)
{
OutputUrl = NSUrl.FromFilename(Path.Combine(Path.GetTempPath(), "temporaryClip/Whole.mov")),
OutputFileType = AVFileType.QuickTimeMovie,
ShouldOptimizeForNetworkUse = true,
VideoComposition = mainComposition
};
exportSession.ExportAsynchronously(_OnExportDone);
}
private AVMutableVideoCompositionLayerInstruction SetInstruction(AVAsset asset, CMTime currentTime, AVMutableCompositionTrack mixComposition_video_Track)
{
//The following code triggers when a device has no camera or no microphone (for instance an emulator)
var instruction = AVMutableVideoCompositionLayerInstruction.FromAssetTrack(mixComposition_video_Track);
//Get the individual AVAsset's track to use for transform
AVAssetTrack assetTrack = asset.TracksWithMediaType(AVMediaType.Video)[0];
//Set transform the the preferredTransform of the AVAssetTrack, not the AVMutableCompositionTrack
CGAffineTransform transform = assetTrack.PreferredTransform;
//Set the transformSize to be the asset natural size AFTER applying preferredTransform.
CGSize transformSize = transform.TransformSize(assetTrack.NaturalSize);
//Handle any negative values resulted from applying transform by using the absolute value
CGSize newAssetSize = new CoreGraphics.CGSize(Math.Abs(transformSize.Width), Math.Abs(transformSize.Height));
//change back to less than
if (newAssetSize.Width < newAssetSize.Height)//portrait
{
/*newAssetSize should no longer be inverted since preferredTransform handles this. Remember that the asset was never
* actually transformed yet. newAssetSize just represents the size the video is going to be after you call
* instruction.setTransform(transform). Since transform is the first transform in concatenation, this is the size that
* the scale and translate transforms will be using, which is why we needed to reference newAssetSize after applying
* transform. Also you should concatenate in this order: transform -> scale -> translate, otherwise you won't get
* desired results*/
nfloat scaleRatio = UIScreen.MainScreen.Bounds.Height / newAssetSize.Height;
//Apply scale to transform. Transform is never actually applied unless you do this.
transform.Scale(scaleRatio, scaleRatio);
nfloat tx = UIScreen.MainScreen.Bounds.Width / 2 - newAssetSize.Width * scaleRatio / 2;
nfloat ty = UIScreen.MainScreen.Bounds.Height / 2 - newAssetSize.Height * scaleRatio / 2;
transform.Translate(tx, ty);
instruction.SetTransform(transform, currentTime);
}
var endTime = CMTime.Add(currentTime, asset.Duration);
instruction.SetOpacity(0, endTime);
return instruction;
}
uj5u.com熱心網友回復:
您在 CMTime.zero 而不是在前一個資產的末尾插入每個時間范圍。另外,您在匯出時是否使用 videoComposition?
更新:很久以前,我在應用程式中播放視頻,所以我實際上并沒有匯出,但是當我第一次開始時,我先匯出,然后將匯出的視頻作為 AVAsset 傳遞到 AVPlayer。從那以后我改變了很多,所以我不會僅僅為了在應用程式中播放而匯出視頻,因為它效率低下并且浪費時間,但是我的代碼在將資產合并在一起方面完美地作業。我在匯出時讓它作業,但從那時起我也改變了我的合并功能,所以不能保證這將與匯出會話一起作業。
func mergeVideos(mixComposition: Binding<AVMutableComposition>, videoComposition: Binding<AVMutableVideoComposition>, mainInstruction: Binding<AVMutableVideoCompositionInstruction>) -> AVPlayerItem {
guard let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
return AVPlayerItem(asset: mixComposition.wrappedValue)
}
//Remove all existing videos, tracks and instructions
self.assets.removeAll()
for track in mixComposition.wrappedValue.tracks {
mixComposition.wrappedValue.removeTrack(track)
}
//Add all videos to asset array
for video in videos {
let url = documentDirectory.appendingPathComponent(video.videoURL)
let asset = AVURLAsset(url: url, options: [AVURLAssetPreferPreciseDurationAndTimingKey : true])
self.assets.append(asset)
}
//add instructions and assets to mixComposition
assets.forEach { asset in
self.addTrackToComposition(asset: asset, mixComposition: mixComposition, videoComposition: videoComposition, mainInstruction: mainInstruction)
}//forEach
//create playerITem with videoComposition
videoComposition.wrappedValue.instructions = [mainInstruction.wrappedValue]
videoComposition.wrappedValue.frameDuration = CMTimeMake(value: 1, timescale: 30)
videoComposition.wrappedValue.renderSize = renderSize
let item = AVPlayerItem(asset: mixComposition.wrappedValue)
item.seekingWaitsForVideoCompositionRendering = true
item.videoComposition = videoComposition.wrappedValue
return item
}//mergeVideo
func addTrackToComposition(asset: AVAsset, mixComposition: Binding<AVMutableComposition>, videoComposition: Binding<AVMutableVideoComposition>, mainInstruction: Binding<AVMutableVideoCompositionInstruction>) {
let currentTime = mixComposition.wrappedValue.duration
guard let assetVideoTrack = mixComposition.wrappedValue.addMutableTrack(withMediaType: .video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else {return}
guard let assetAudioTrack = mixComposition.wrappedValue.addMutableTrack(withMediaType: .audio, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else {return}
do {
let timeRange = CMTimeRangeMake(start: .zero, duration: asset.duration)
// Insert video to Mutable Composition at right time.
try assetVideoTrack.insertTimeRange(timeRange, of: asset.tracks(withMediaType: .video)[0], at: currentTime)
try assetAudioTrack.insertTimeRange(timeRange, of: asset.tracks(withMediaType: .audio)[0], at: currentTime)
let videoInstruction = videoCompositionInstruction(track: assetVideoTrack, asset: asset, currentTime: currentTime)
mainInstruction.wrappedValue.layerInstructions.append(videoInstruction)
mainInstruction.wrappedValue.timeRange = CMTimeRange(start: .zero, duration: mixComposition.wrappedValue.duration)
} catch let error {
print(error.localizedDescription)
}
}//addTrackToComposition
func videoCompositionInstruction(track: AVCompositionTrack, asset: AVAsset, currentTime: CMTime) -> AVMutableVideoCompositionLayerInstruction {
let instruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track)
guard let assetTrack = asset.tracks(withMediaType: .video).first else { return instruction }
let transform = assetTrack.preferredTransform
let transformSize = assetTrack.naturalSize.applying(transform) //for export session
let newAssetSize = CGSize(width: abs(transformSize.width), height: abs(transformSize.height)) // for export session
if newAssetSize.width < newAssetSize.height { //portrait
let scaleRatio = renderSize.height / newAssetSize.height
let scale = CGAffineTransform(scaleX: scaleRatio, y: scaleRatio)
let tx = renderSize.width / 2 - newAssetSize.width * scaleRatio / 2
let ty = renderSize.height / 2 - newAssetSize.height * scaleRatio / 2
let translate = CGAffineTransform(translationX: tx, y: ty)
let concatenation = transform.concatenating(scale).concatenating(translate)
instruction.setTransform(concatenation, at: currentTime)
} else if newAssetSize.width > newAssetSize.height { //landscape
let scaleRatio = renderSize.width / newAssetSize.width
let scale = CGAffineTransform(scaleX: scaleRatio, y: scaleRatio)
let tx = renderSize.width / 2 - newAssetSize.width * scaleRatio / 2
let ty = renderSize.height / 2 - newAssetSize.height * scaleRatio / 2
let translate = CGAffineTransform(translationX: tx, y: ty)
let concatenation = transform.concatenating(scale).concatenating(translate)
instruction.setTransform(concatenation, at: currentTime)
} else if newAssetSize.width == newAssetSize.height {
//if landscape, fill height first, if portrait fill width first, if square doesnt matter just scale either width or height
if renderSize.width > renderSize.height { //landscape
let scaleRatio = renderSize.height / newAssetSize.height
let scale = CGAffineTransform(scaleX: scaleRatio, y: scaleRatio)
let tx = renderSize.width / 2 - newAssetSize.width * scaleRatio / 2
let ty = renderSize.height / 2 - newAssetSize.height * scaleRatio / 2
let translate = CGAffineTransform(translationX: tx, y: ty)
let concatenation = transform.concatenating(scale).concatenating(translate)
instruction.setTransform(concatenation, at: currentTime)
} else { //portrait and square
let scaleRatio = renderSize.width / newAssetSize.width
let scale = CGAffineTransform(scaleX: scaleRatio, y: scaleRatio)
let tx = renderSize.width / 2 - newAssetSize.width * scaleRatio / 2
let ty = renderSize.height / 2 - newAssetSize.height * scaleRatio / 2
let translate = CGAffineTransform(translationX: tx, y: ty)
let concatenation = transform.concatenating(scale).concatenating(translate)
instruction.setTransform(concatenation, at: currentTime)
}
}
let endTime = CMTimeAdd(currentTime, asset.duration)
instruction.setOpacity(0, at: endTime)
return instruction
}//videoCompositionInstruction
我將簡要說明我在這里做什么。
您不需要為 AVMutableComposition、AVMutableVideoComposition 或 AVMutableVideoCompositionInstructions 傳入系結。我只為我的應用程式中的某些功能這樣做。在執行任何其他操作之前,您可以在函式中實體化所有這些。
我在類中有一個陣列來保存我所有的資產,這就是 self.assets 。“視頻”參考了一個領域模型,我用來存盤用戶從他們的照片庫中挑選的視頻的最后一個路徑組件。您可能不需要洗掉所有現有的視頻、曲目和說明,因為您沒有傳遞對樂曲和說明的參考。我這樣做是因為我在整個應用程式中對這些物件進行了更改。您也不需要使用任何wrappedValues,因為這僅用于系結。
一旦我填充了我的資產陣列,我就會遍歷它,呼叫 addTrackToComposition 并傳入每個資產。此函式將音頻和視頻軌道添加到每個資產的 mixComposition。然后在 do-catch 塊中,它嘗試將資產音頻和視頻軌道插入您剛剛為 mixComposition 創建的空 mutableTracks。所以 mixComposition 將為每個資產(一個音頻和一個視頻)提供 2 個軌道。我這樣做是為了我可以更好地控制我的指令并將不同的轉換應用于每個資產而不是整個 mixComposition 作為一個整體。您也可以只為 for 回圈外的 mixComposition 創建空的 mutableTracks 并將資產的軌道插入該軌道(實際上是兩個軌道 - 音頻/視頻)。我知道嘗試慢慢分解這聽起來令人困惑。需要注意的是,在我的 do-catch 塊中,我傳遞的 timeRange 是資產時間范圍,但我將其插入到 mixComposition (currentTime = mixComposition.duration) 的末尾。這就是為什么 timeRange 從 kCMTimeZero (.zero) 開始,但我為 at: 引數傳入 currentTime。
然后我使用一個函式為每個資產創建層指令。這會縮放和定位每個資產,以便它在我的自定義視頻播放器中正確顯示。它還在資產結束時將不透明度設定為 0。這里我的 renderSize 在我的 Realm 模型中宣告并且是一個 CGSize(width: 1280, height: 720)。現在,我不確定這些轉換是否適用于您的用例,但我知道您肯定需要轉換,否則您的資產將以錯誤的方向和/或大小/位置匯出。至少您需要設定資產preferredTrackTransform。使用 AVAssetTrack 的 preferredTransform 而不是 AVCompositionTrack 的 preferredTransform 很重要。這會為您處理方向,但不會處理比例和位置。玩弄它。
然后我將層指令附加到 mainInstruction 并將 mainInstructions timeRange 設定為等于 mixCompositions timeRange。我不知道為什么我在 for 回圈的每次迭代中都設定 timeRange 并且我絕對可以在添加所有指令和軌道之后才這樣做,所以它只發生一次而不是每次迭代。
最后,我將 videoCompositions 指令設定為僅包含 mainInstruction 的陣列,并設定幀速率和渲染大小。希望當您將其傳遞到匯出會話時,所有這些都對您有用。
看看您嘗試實作它的方式,我會說您不需要 layerInstructions 陣列。只需創建一個 AVMutableVideoCompositionInstruction 物件(mainInstruction)并將層指令附加到該物件。
Also there is a problem with you using previous asset duration. You need to pass in mixCompositions duration when you insert the new asset's time range. What you are doing is inserting at just the previous assets duration so you are ending up with a bunch of overlapping assets. You want to insert it after all previous assets duration combined, which would mixCompositions current duration.
Also mainInstruction should not be a List either. It should just be an AVMutableVideoCompositionInstruction(). AVMutableVideoCompositionInstruction has a layerInstructions property that is an array of layerInstructions. You can append directly to this. There should not be more than one mainInstruction. There should only be multiple layerInstructions.
Be patient with this. It took me a very long time to figure out myself, coming from no AVFoundation experience. I honestly still don't know enough to be sure of what's wrong with your current code, but all I know is that this works for me. Hopefully, it works for you too. I've probably changed this function 20 times since I started this app a couple months ago.
UPDATE: So you are on the right path, but there are still a few problems that may be the cause of your issue.
1.) I forgot to mention this last time, but when I was faced with the same problem, multiple people told me that I HAD to handle the audio track separately. Apparently even the video won't work without doing this. I never actually tested to see if this was true, but it's worth a shot. You can refer to my code again to see how I handled the audio track. It is essentially the same thing as the video track but you don't apply any instructions to it.
2.) In your instructions function there are few problems. Your instruction property is correct, but your transform, transformSize, and newAssetSize are not correct. Currently you set transform to assetTrack.preferredTransform. This is actually the mixComposition's transform, but what you want to use is the original AVAsset's preferredTransform.
After you initialize your instruction using assetTrack (mixComposition's track), you need to declare a new property to get the AVAsset's track. Refer back to my code. I actually use the name "assetTrack" to do this so don't be confused with our variable names. Your "assetTrack" is my "track" which I passed in as a parameter. My "assetTrack" is what you need to add, but obviously you can use whatever name you want.
So videos are a little strange when recorded on our devices. A video recorded in portrait orientation is actually landscape. Each asset, however, comes with data that informs the device how it should be displayed (i.e. rotate video so it displays the same way it was recorded). That is what preferredTransform is. It will transform the asset to display in the correct orientation. This is why you need to make sure you are using each individual asset's preferredTransform and not the mixComposition's preferredTransform that you used in your code. The mixComposition's preferredTransform will just be an identity matrix which effectively doesn't do anything at all. This is why your asset's natural size is "inverted". It is not inverted, that is just the way apple stores all videos and pictures. The meta data handles the correct orientation which is in preferredTransform and that will result in the "correct" width and height.
So now that you have the correct transform stored in your transform property, your transformSize property needs to reflect this, however you forget to add "applying(transform)" to the size. This is important. The transformSize you currently have is just the naturalSize, whereas, you want the size after applying the transform to the asset (so that width and height actually reflect the correct orientation of the video).
So now newAssetSize is meant to handle any negative values that are resulted from transformSize. So when you create newAssetSize, you need to make sure you are using the Absolute value of transformSize.width and transformSize.height. That is why I had it as "abs(transformSize.width)" in my code. This is also crucial.
3.) You never applied the preferredTransform to the video, and you instead apply scale transform to a matrix of all 0, which is never going to work. At very least you need to concatenate scale and translate to the identity matrix, although you should really be concatenating them to transform instead. If you don't change this part your video will never display no matter what you do. Any transforms you concatenate with on a zero matrix will have no effect and you will still result in a 0 matrix which means your video will not display at all.
Try to make these changes, especially the changes in the instruction function. I believe you will also have to redo your transform logic after you change those properties as it looks like you tried to compensate for the fact that width and height were inverted.
Your code should look something like this (keep in mind that I am not familiar with c# at all):
public void mergeclips()
{
AVMutableComposition mixComposition = new AVMutableComposition();
AVMutableVideoCompositionLayerInstruction[] Instruction_Array = new AVMutableVideoCompositionLayerInstruction[Clips.Count];
foreach (string clip in Clips)
{
#region HoldVideoTrack
AVAsset asset = AVAsset.FromUrl(NSUrl.FromFilename(clip));
AVMutableCompositionTrack videoTrack = mixComposition.AddMutableTrack(AVMediaType.Video, 0);
AVMutableCompositionTrack audioTrack = mixComposition.AddMutableTrack(AVMediaType.Audio, 0);
CMTimeRange range = new CMTimeRange()
{
Start = new CMTime(0, 0),
Duration = asset.Duration
};
AVAssetTrack assetVideoTrack = asset.TracksWithMediaType(AVMediaType.Video)[0];
videoTrack.InsertTimeRange(range, assetVideoTrack, mixComposition.Duration, out NSError error);
AVAssetTrack assetAudioTrack = asset.TracksWithMediaType(AVMediaType.Audio)[0];
audioTrack.InsertTimeRange(range, assetAudioTrack, mixComposition.Duration, out NSError error);
#endregion
#region Instructions
Instruction_Array[Clips.IndexOf(clip)] = SetInstruction(asset, mixComposition.Duration, videoTrack);
#endregion
}
// 6
var mainInstruction = new AVMutableVideoCompositionInstruction();
CMTimeRange rangeIns = new CMTimeRange()
{
Start = new CMTime(0, 0),
Duration = mixComposition.Duration
};
mainInstruction.BackgroundColor = UIColor.FromRGBA(1f, 1f, 1f, 1.000f).CGColor;
mainInstruction.TimeRange = rangeIns;
mainInstruction.LayerInstructions = Instruction_Array;
var mainComposition = new AVMutableVideoComposition()
{
Instructions = new AVVideoCompositionInstruction[1] { mainInstruction },
FrameDuration = new CMTime(1, 30),
RenderSize = new CoreGraphics.CGSize(UIScreen.MainScreen.Bounds.Width, UIScreen.MainScreen.Bounds.Height)
};
//... export video ...
AVAssetExportSession exportSession = new AVAssetExportSession(mixComposition, AVAssetExportSessionPreset.MediumQuality)
{
OutputUrl = NSUrl.FromFilename(Path.Combine(Path.GetTempPath(), "temporaryClip/Whole.mov")),
OutputFileType = AVFileType.QuickTimeMovie,
ShouldOptimizeForNetworkUse = true,
VideoComposition = mainComposition
};
exportSession.ExportAsynchronously(_OnExportDone);
}
private AVMutableVideoCompositionLayerInstruction SetInstruction(AVAsset asset, CMTime currentTime, AVMutableCompositionTrack assetTrack)
{
var instruction = AVMutableVideoCompositionLayerInstruction.FromAssetTrack(assetTrack);
//Get the individual AVAsset's track to use for transform
AVAssetTrack avAssetTrack = asset.TracksWithMediaType(AVMediaType.Video)[0];
//Set transform the the preferredTransform of the AVAssetTrack, not the AVMutableCompositionTrack
var transform = avAssetTrack.PreferredTransform;
//Set the transformSize to be the asset natural size AFTER applying preferredTransform.
var transformSize = avAssetTrack.NaturalSize.applying(transform);
//Handle any negative values resulted from applying transform by using the absolute value
var newAssetSize = new CoreGraphics.CGSize(Abs(transformSize.Width), Abs(transformSize.Height)); // for export session
//change back to less than
if (newAssetSize.Width < newAssetSize.Height)//portrait
{
//newAssetSize should no longer be inverted since preferredTransform handles this. Remember that the asset was never actually transformed yet. newAssetSize just represents the size the video is going to be after you call instruction.setTransform(transform). Since transform is the first transform in concatenation, this is the size that the scale and translate transforms will be using, which is why we needed to reference newAssetSize after applying transform. Also you should concatenate in this order: transform -> scale -> translate, otherwise you won't get desired results
var scaleRatio = UIScreen.MainScreen.Bounds.Height / newAssetSize.Height; //change back to height. Keep in mind that this scaleRatio will fill the height of the screen first and the width will probably exceed the screen bounds. I had it set like this because I was displaying my video in a view that is much smaller than the screen size. If you want to display the video centered on the phone screen, try using scaleRation = UIScreen.MainScreen.Bounds.Width / newAssetSize.Width. This will scale the video to fit the width of the screen perfectly and then the height will be whatever it is with respect to the videos aspect ratio.
//Apply scale to transform. Transform is never actually applied unless you do this.
var _coreGraphic = transform.Scale(scaleRatio, scaleRatio);
var tx = UIScreen.MainScreen.Bounds.Width / 2 - newAssetSize.Height * scaleRatio / 2;
var ty = UIScreen.MainScreen.Bounds.Height / 2 - newAssetSize.Width * scaleRatio / 2;
_coreGraphic.Translate(tx, ty);
instruction.SetTransform(_coreGraphic, currentTime);
}
var endTime = CMTime.Add(currentTime, asset.Duration);
instruction.SetOpacity(0, endTime);
return instruction;
}
uj5u.com熱心網友回復:
好的,感謝 Shawn 的幫助,我已經完成了我想要做的事情。我的代碼中有兩個主要錯誤導致了這個問題,第一個錯誤是如何設定給 VideoTrack 的 CMTime 屬性:Start = new CMTime(0,0),而不是Start = new CMTime.Zero,. 我仍然不知道它有什么區別,但它阻止代碼顯示每個資產的視頻和音頻,留下一個視頻,其中包含所有剪輯的長度和 AVMutableVideoCompositionInstruction 的背景。第二個錯誤是我如何設定指令,可以在以下代碼中找到對我有用的配置。
這是最終正常作業的函式:
public void MergeClips()
{
//microphone
AVCaptureDevice microphone = AVCaptureDevice.DefaultDeviceWithMediaType(AVMediaType.Audio);
AVMutableComposition mixComposition = AVMutableComposition.Create();
AVVideoCompositionLayerInstruction[] Instruction_Array = new AVVideoCompositionLayerInstruction[Clips.Count];
foreach (string clip in Clips)
{
var asset = AVUrlAsset.FromUrl(new NSUrl(clip, false)) as AVUrlAsset;
#region HoldVideoTrack
//This range applies to the video, not to the mixcomposition
CMTimeRange range = new CMTimeRange()
{
Start = CMTime.Zero,
Duration = asset.Duration
};
var duration = mixComposition.Duration;
NSError error;
AVMutableCompositionTrack videoTrack = mixComposition.AddMutableTrack(AVMediaType.Video, 0);
AVAssetTrack assetVideoTrack = asset.TracksWithMediaType(AVMediaType.Video)[0];
videoTrack.InsertTimeRange(range, assetVideoTrack, duration, out error);
videoTrack.PreferredTransform = assetVideoTrack.PreferredTransform;
if (microphone != null)
{
AVMutableCompositionTrack audioTrack = mixComposition.AddMutableTrack(AVMediaType.Audio, 0);
AVAssetTrack assetAudioTrack = asset.TracksWithMediaType(AVMediaType.Audio)[0];
audioTrack.InsertTimeRange(range, assetAudioTrack, duration, out error);
}
#endregion
#region Instructions
int counter = Clips.IndexOf(clip);
Instruction_Array[counter] = SetInstruction(asset, mixComposition.Duration, videoTrack);
#endregion
}
// 6
AVMutableVideoCompositionInstruction mainInstruction = AVMutableVideoCompositionInstruction.Create() as AVMutableVideoCompositionInstruction;
CMTimeRange rangeIns = new CMTimeRange()
{
Start = new CMTime(0, 0),
Duration = mixComposition.Duration
};
mainInstruction.TimeRange = rangeIns;
mainInstruction.LayerInstructions = Instruction_Array;
var mainComposition = AVMutableVideoComposition.Create();
mainComposition.Instructions = new AVVideoCompositionInstruction[1] { mainInstruction };
mainComposition.FrameDuration = new CMTime(1, 30);
mainComposition.RenderSize = new CGSize(mixComposition.NaturalSize.Height, mixComposition.NaturalSize.Width);
finalVideo_path = NSUrl.FromFilename(Path.Combine(Path.GetTempPath(), "Whole2.mov"));
if (File.Exists(Path.GetTempPath() "Whole2.mov"))
{
File.Delete(Path.GetTempPath() "Whole2.mov");
}
//... export video ...
AVAssetExportSession exportSession = new AVAssetExportSession(mixComposition, AVAssetExportSessionPreset.HighestQuality)
{
OutputUrl = NSUrl.FromFilename(Path.Combine(Path.GetTempPath(), "Whole2.mov")),
OutputFileType = AVFileType.QuickTimeMovie,
ShouldOptimizeForNetworkUse = true,
VideoComposition = mainComposition
};
exportSession.ExportAsynchronously(_OnExportDone);
}
private AVMutableVideoCompositionLayerInstruction SetInstruction(AVAsset asset, CMTime currentTime, AVAssetTrack mixComposition_video_Track)
{
var instruction = AVMutableVideoCompositionLayerInstruction.FromAssetTrack(mixComposition_video_Track);
var startTime = CMTime.Subtract(currentTime, asset.Duration);
//NaturalSize.Height is passed as a width parameter because IOS stores the video recording horizontally
CGAffineTransform translateToCenter = CGAffineTransform.MakeTranslation(mixComposition_video_Track.NaturalSize.Height, 0);
//Angle in radiants, not in degrees
CGAffineTransform rotate = CGAffineTransform.Rotate(translateToCenter, (nfloat)(Math.PI / 2));
instruction.SetTransform(rotate, (CMTime.Subtract(currentTime, asset.Duration)));
instruction.SetOpacity(1, startTime);
instruction.SetOpacity(0, currentTime);
return instruction;
}
正如我所說,由于 Shawn 的幫助,我解決了我的問題,并且大部分代碼都從他的答案中轉換為 C#,所以如果你打算投票給這個答案,請投票給 Shawn 的一個,或者兩者都投票。
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/381501.html
