短视频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>