短视频SDK提供了视频拼接接口AliyunIMixComposer。该接口实现离线多画面合并成一个视频的功能,例如画中画、九宫格、左右分屏、上下分屏等视频效果,支持添加多轨道视频。本文为您介绍Android端视频拼接的流程以及示例代码。
版本支持
版本 | 是否支持 |
---|---|
专业版 | 支持 |
标准版 | 支持 |
基础版 | 不支持 |
相关类功能
类名 | 功能 |
---|---|
AliyunIMixComposer | 拼接功能核心类,包括设置输出参数、创建轨道、添加视频流、开始拼接、设置回调等拼接核心功能。 |
AliyunMixComposerCreator | 工厂类,创建一个AliyunIMixComposer实现类的实例。 |
AliyunMixTrack | 视频轨道,用于在轨道上添加视频流,设置轨道的布局、音量等参数。 |
AliyunMixStream | 视频轨道流,用于获取视频流的显示模式、文件路径、结束时间等。 |
AliyunMixOutputParam | 拼接输出参数,设置拼接视频的宽高、输出码率、质量等级等参数。 |
AliyunMixCallback | 拼接回调,设置拼接完成、拼接进度及拼接失败的回调。 |
拼接结构图
拼接流程
阶段 | 流程 | 说明 | 示例代码 |
---|---|---|---|
基础 | 1 | 创建拼接实例。 | 创建实例 |
2 | 创建多个轨道,创建视频流,再将视频流分别添加到各轨道上。 | 创建轨道 | |
3 | 配置视频拼接后的输出路径、视频宽高等参数。 | 配置输出参数 | |
4 | 设置回调,开始拼接。 | 开始拼接 | |
5 | 销毁接口,释放资源。 | 释放资源 | |
进阶 | 6 | 取消、暂停、继续拼接,按需设置。 | 拼接控制 |
创建实例
创建拼接实例。
代码中需要使用的参数详情,请参考接口文档。接口链接请参见相关类功能。
//创建实例
AliyunIMixComposer mixComposer = AliyunMixComposerCreator.createMixComposerInstance();
创建轨道
创建多个轨道,创建视频流,再将视频流分别添加到各轨道上。代码中需要使用的参数详情,请参考接口文档。接口链接请参见相关类功能
//创建轨道
//创建轨道1
//轨道1的布局
AliyunMixTrackLayoutParam track1Layout = new AliyunMixTrackLayoutParam.Builder()
.centerX(0.25f)
.centerY(0.5f)
.widthRatio(0.5f)
.heightRatio(1.f)
.build();
//创建轨道1实例
AliyunMixTrack track1 = mixComposer.createTrack(track1Layout);
// 创建轨道1的第一个视频流1
AliyunMixStream stream11 = new AliyunMixStream
.Builder()
.displayMode(VideoDisplayMode.FILL)
.filePath("/storage/emulated/0/lesson_01.mp4")
.streamEndTimeMills(20000)
.build();
//添加该视频流到轨道1, 注意:只能添加一个,以最后添加的为准
track1.addStream(stream11);
//创建轨道2
//轨道2参数
AliyunMixTrackLayoutParam track2Layout = new AliyunMixTrackLayoutParam.Builder()
.centerX(0.75f)
.centerY(0.5f)
.widthRatio(0.5f)
.heightRatio(1.f)
.build();
//创建轨道2实例
AliyunMixTrack track2 = mixComposer.createTrack(track2Layout);
//创建轨道2的第一个视频流1
AliyunMixStream stream21 = new AliyunMixStream
.Builder()
.displayMode(VideoDisplayMode.FILL)
.filePath("/storage/emulated/0/lesson_02.mp4")
.streamStartTimeMills(10000)
.streamEndTimeMills(30000)
.build();
//添加该视频流到轨道2, 注意:只能添加一个,以最后添加的为准
track2.addStream(stream21);
配置输出参数
配置视频拼接后的输出路径、视频宽高等参数。代码中需要使用的参数详情,请参考接口文档。接口链接请参见相关类功能
//配置输出参数
AliyunMixOutputParam outputParam = new AliyunMixOutputParam.Builder()
.outputPath("/sdcard/output.mp4") //拼接后的路径
.outputAudioReferenceTrack(track2)//表示使用轨道2的音频作为最后的音频,目前音频轨道只支持一个音频流
.outputDurationReferenceTrack(track2)//表示使用轨道2的时长作为最后输出视频的时长,如果轨道1的时长不够,则会停在最后一帧
.crf(6)
.videoQuality(VideoQuality.HD)
.outputWidth(720) //视频宽度
.outputHeight(1280) //视频高度
.fps(30) //fps
.gopSize(30) //gop
.build();
mixComposer.setOutputParam(outputParam);
开始拼接
设置回调,开始拼接视频。代码中需要使用的参数详情,请参考接口文档。接口链接请参见相关类功能
//开始拼接
AliyunMixCallback callback = new AliyunMixCallback() {
@Override
public void onProgress(long progress) {//拼接进度
Log.e("MixRecord", "onProgress " + progress);
}
@Override
public void onComplete() {
Log.e("MixRecord", "onComplete"); //拼接完成
runOnUiThread(new Runnable() {
@Override
public void run() {
//该接口一定不能在回调的线程中直接调用!!!
//拼接完成后释放实例
mixComposer.release();
}
});
}
@Override
public void onError(int errorCode) { //拼接失败
Log.e("MixRecord", "onError " + errorCode);
}
};
释放资源
拼接完成后销毁实例,释放资源。请务必注意不要在拼接过程中销毁实例。代码中需要使用的参数详情,请参考接口文档。接口链接请参见相关类功能
mixComposer.release();
拼接控制
按需设置取消、暂停、继续拼接。代码中需要使用的参数详情,请参考接口文档。接口链接请参见相关类功能
//暂停拼接
mixComposer.pause();
//继续拼接
mixComposer.resume();
//取消拼接
mixComposer.cancel();
视频拼接代码示例
/**
* 视频拼接Example
*/
class MixActivity : AppCompatActivity() {
private val REQUEST_TRACK1_STREAM = 1001
private val REQUEST_TRACK2_STREAM = 1002
private var 示例 : AliyunIMixComposer? = null
private lateinit var mVideoTrack1 : AliyunMixTrack
private lateinit var mVideoTrack2 : AliyunMixTrack
private var mVideoTrack1Duration = 0L
private var mVideoTrack2Duration = 0L
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_mix)
findViewById<Button>(R.id.btnReset).setOnClickListener {
findViewById<Button>(R.id.btnMix).isEnabled = false
mMixComposer?.release()
init()
}
findViewById<Button>(R.id.btnAddTrack1Stream).setOnClickListener {
PermissionX.init(this)
.permissions(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
.request { allGranted, _, _ ->
if (allGranted) {
PictureSelector.create(this)
.openGallery(PictureMimeType.ofVideo())
.forResult(REQUEST_TRACK1_STREAM)
}
}
}
findViewById<Button>(R.id.btnAddTrack2Stream).setOnClickListener {
PermissionX.init(this)
.permissions(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
.request { allGranted, _, _ ->
if (allGranted) {
PictureSelector.create(this)
.openGallery(PictureMimeType.ofVideo())
.forResult(REQUEST_TRACK2_STREAM)
}
}
}
findViewById<Button>(R.id.btnMix).setOnClickListener {
//开始合成
val callback: AliyunMixCallback = object : AliyunMixCallback {
override fun onProgress(progress: Long) { //合成进度
Log.e("MixActivity", "onProgress $progress")
}
override fun onComplete() {
Log.e("MixActivity", "onComplete")
ToastUtil.showToast(it.context, "视频合成成功")
}
override fun onError(errorCode: Int) {
Log.e("MixActivity", "onError $errorCode")
ToastUtil.showToast(it.context, "视频合成失败:$errorCode")
}
}
//配置输出参数
val outputParamBuilder = AliyunMixOutputParam.Builder()
outputParamBuilder
.outputPath("/storage/emulated/0/DCIM/Camera/svideo_mix_demo.mp4")
.crf(6)
.videoQuality(VideoQuality.HD)
.outputWidth(720)
.outputHeight(1280)
.fps(30)
.gopSize(30)
if(mVideoTrack1Duration > mVideoTrack2Duration) {
outputParamBuilder.outputAudioReferenceTrack(mVideoTrack1)
outputParamBuilder.outputDurationReferenceTrack(mVideoTrack1)
} else {
outputParamBuilder.outputAudioReferenceTrack(mVideoTrack2)
outputParamBuilder.outputDurationReferenceTrack(mVideoTrack2)
}
mMixComposer!!.setOutputParam(outputParamBuilder.build())
mMixComposer!!.start(callback)
}
init()
}
private fun init() {
mMixComposer = AliyunMixComposerCreator.createMixComposerInstance()
//创建轨道1
val track1Layout = AliyunMixTrackLayoutParam.Builder()
.centerX(0.25f)
.centerY(0.25f)
.widthRatio(0.5f)
.heightRatio(0.5f)
.build()
mVideoTrack1 = mMixComposer!!.createTrack(track1Layout)
//创建轨道2
val track2Layout = AliyunMixTrackLayoutParam.Builder()
.centerX(0.75f)
.centerY(0.75f)
.widthRatio(0.5f)
.heightRatio(0.5f)
.build()
mVideoTrack2 = mMixComposer!!.createTrack(track2Layout)
}
override fun onResume() {
super.onResume()
mMixComposer?.resume()
}
override fun onPause() {
super.onPause()
mMixComposer?.pause()
}
override fun onDestroy() {
super.onDestroy()
mMixComposer?.release()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
REQUEST_TRACK1_STREAM -> {
// onResult Callback
val result = PictureSelector.obtainMultipleResult(data)
mVideoTrack1Duration = 0
for (streamBean in result) {
// 创建轨道1的第一个视频流1
val stream1 = AliyunMixStream.Builder()
.displayMode(VideoDisplayMode.FILL)
.filePath(streamBean.realPath)
.streamStartTimeMills(mVideoTrack1Duration)
.streamEndTimeMills(streamBean.duration)
.build()
mVideoTrack1Duration += streamBean.duration
if(mVideoTrack1.addStream(stream1) == 0) {
ToastUtil.showToast(this, "添加轨道1视频流成功")
}
}
}
REQUEST_TRACK2_STREAM -> {
// onResult Callback
val result = PictureSelector.obtainMultipleResult(data)
mVideoTrack2Duration = 0L
for (streamBean in result) {
// 创建轨道2的第一个视频流1
val stream1 = AliyunMixStream.Builder()
.displayMode(VideoDisplayMode.FILL)
.filePath(streamBean.realPath)
.streamStartTimeMills(mVideoTrack2Duration)
.streamEndTimeMills(streamBean.duration)
.build()
mVideoTrack2Duration += streamBean.duration
if(mVideoTrack2.addStream(stream1) == 0) {
ToastUtil.showToast(this, "添加轨道2视频流成功")
}
}
}
}
}
if(mVideoTrack1Duration > 0 && mVideoTrack2Duration > 0) {
findViewById<Button>(R.id.btnMix).isEnabled = true
}
}
}
XML文件配置示例
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btnReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="76dp"
android:text="重置"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnAddTrack1Stream"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="56dp"
android:layout_marginTop="64dp"
android:text="添加视频1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnReset"
app:layout_constraintEnd_toEndOf="@id/btnReset"
/>
<Button
android:id="@+id/btnAddTrack2Stream"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="64dp"
android:layout_marginEnd="56dp"
android:text="添加视频2"
app:layout_constraintStart_toStartOf="@id/btnReset"
app:layout_constraintTop_toBottomOf="@id/btnReset"
app:layout_constraintEnd_toEndOf="parent"
/>
<Button
android:id="@+id/btnMix"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="176dp"
android:text="开始合成"
android:enabled="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>