支持Android 5.0或以上的设备,暂不支持模拟器调试
确保申请到有效的License
确保申请到Maven仓库的账号密码:获取方法请联系和您对接的技术支持经理
版本号:5.0.1-rc.5-v130
Name | Coordinate | 说明 |
---|---|---|
CutSame SDK | com.volcengine.ck.cutsame:CK-CutSameIF | 剪同款模版消费SDK,负责模版的重演,更新,合成能力。 |
//build.gradle dependencies { implementation "com.volcengine.ck.cutsame:CK-CutSameIF:5.0.1-rc.5-v130" }
获取的数据主要为模版信息(模版标题,模版预览视频等)。主要两个作用,第一提供页面展示信息数据,第二构造后续模版重演流程需要的数据TemplateItem
后续素材选择和模版重演播放均需要模版槽位信息,模版槽位信息包括素材路径,素材类型,materialId(模版的槽位id)等信息,详细见获取模版媒体素材部分MediaItem的定义
获取模板槽位信息方式:通过CutSameSource的prepareSource()方法获取到List <MediaItem>
主要填充MediaItem中的相关字段,如source,mediaSrcPath,type,oriDuration。字段含义详情见获取模版媒体素材部分MediaItem的定义。主要关注需要业务赋值部分
经过填充模版槽位信息步骤后,需要调用CutSameSource.composeSource()接口进行素材合成处理,主要进行以下处理:
对素材进行压缩处理,减少素材分辨率,提高重演效率。
根据模版制作规则,按需对素材进行倒放处理。
经过上述步骤后,将List
设置鉴权配置,SDK鉴权相关的配置
将离线鉴权文件放置到宿主工程/app/src/main/assets/License/目录下
拷贝离线鉴权文件到App私有目录下
// 根据文件名,拷贝离线鉴权文件到App私有目录下 val licenseFileName = "com.volcengine.effectone.licbag" val licenseFile = File(EOUtils.pathUtil.internalLicense(), licenseFileName) if (!licenseFile.exists()) { EOUtils.fileUtil.copyAssetFile("license/${licenseFileName}", licenseFile.absolutePath) }
val authConfig = EOAuthConfig() authConfig.offlineLicensePath = "鉴权文件本地路径" val result = EOAuthorization.makeAuthWithConfig(authConfig) if (result.resCode != EOAuthErrCode.EO_AUTH_SUCCESS) { //鉴权失败 } else { //鉴权成功 } //cutSameLicensePath即有可用的剪同款授权文件 val cutSameLicensePath = EOAuthorizationInternal.getCutSameLicensePath()
鉴权初始化成功以后,初始化剪同款SDK相关的配置
EffectOneConfigList.configure(CutSameUIConfig()) { //剪同款模板内置或者下发可以通过在这配置OnlineResourceLoader或OfflineResourceLoader it.resourceLoader = OnlineResourceLoader.instance } CutSameInit.init()
获取剪同款的模板列表,用于展示和构造TemplateItem用于模板解析
//获取配置的CutSameUIConfig private val cutsameConfig: CutSameUIConfig? by lazy { InnerEffectOneConfigList.getConfig() } //获取模板列表 val data = withContext(Dispatchers.IO) { val key = EOResourcePanelKey.CUTSAME_TEMPLATES.value cutsameConfig?.resourceLoader?.loadResourceList(key, key) ?: emptyList() }
剪同款模板素材如何使用素材SDK进行管理,参考:素材管理
根据模板包的路径,构造一个TemplateItem用于模板重演
data class TemplateItem( @SerializedName("title") val title: String = "", @SerializedName("fragment_count") val fragmentCount: Int = 0, @SerializedName("md5") val md5: String = "", @SerializedName("zip_path") var zipPath: String = "", //zip文件的本地路径,用于离线模板 @SerializedName("unzip_path") var unZipPath: String = "", //zip文件解压后的本地路径,用于离线模板 @SerializedName("fragments") var fragments: List<TemplateFragment> = emptyList() //槽位的信息 )
val templateItem = TemplateItem( md5 = pathMd5, unZipPath = result.absPath )
提供素材处理接口,一个模板视频制作周期内,使用同一个模板对象
创建模板对象,首次创建CutSameSource对象时需要该对象
/** * 模版资源对象 * 模板支持传入zip包或者解压后的文件夹,两个字段二选一传入即可,都传的话优先使用zipPath * @param md5: 模版文件md5 * @param zipPath * @param unzipPath */ data class SourceInfo( val md5: String, val zipPath: String = "", val unzipPath: String = "" )
/** * @param sourceInfo: 模版资源对象 */ fun createCutSameSource(sourceInfo: SourceInfo): CutSameSource
//调用该接口使用SourceInfo对象创建CutSameSource对象 cutSameSource = CutSameSolution.createCutSameSource( SourceInfo( templateItem.md5, templateItem.zipPath, templateItem.unZipPath ) )
解析模板,获取模板的槽位信息
fun prepareSource( listener: PrepareSourceListener) interface PrepareSourceListener { //模版准备进度 fun onProgress(progress: Float) //模版准备成功,返回槽位信息 fun onSuccess(mediaItemList: ArrayList<MediaItem>?, textItemList: ArrayList<TextItem>?, model: NLEModel ) //模版准备失败 fun onError(code: Int, message: String?) }
cutSameSource?.prepareSource(object : PrepareSourceListener { override fun onProgress(progress: Float) { } override fun onSuccess( mediaItemList: ArrayList<MediaItem>?, textItemList: ArrayList<TextItem>?, model: NLEModel ) { //填充mediaItemList的相关字段 } override fun onError(code: Int, message: String?) { } })
选择素材后,对选择的素材进行压缩、倒放等处理
/** * @param processMediaItems: 选择后的素材列表 * @param compressSourceListener: 素材处理状态接口 */ fun composeSource( processMediaItems: ArrayList<MediaItem>, compressSourceListener: ComposeSourceListener )
//素材处理状态接口 interface ComposeSourceListener { // 处理失败 fun onError(errorCode: Int, message: String?) // 处理进度 fun onProgress(progress: Int) // 处理成功 fun onSuccess(mediaItems: ArrayList<MediaItem>?) }
cutSameSource?.composeSource(data, object : ComposeSourceListener { override fun onError(errorCode: Int, message: String?) { //处理素材失败 } override fun onProgress(progress: Int) { //处理素材进度 } override fun onSuccess(mediaItems: ArrayList<MediaItem>?) { //处理素材成功 } })
创建CutSamePlayer对象,此对象提供视频预览、槽位编辑、文本编辑、视频导出等能力
/** * 创建CutSamePlayer * @param surfaceView: 播放对象 * @param cutSameSource: 素材操作对象,如果是草稿导入的情况,此参数可空 */ fun createCutSamePlayer(surfaceView: SurfaceView, cutSameSource: CutSameSource?): CutSamePlayer
cutSamePlayer = CutSameSolution.createCutSamePlayer(videoSurfaceView, cutSameSource)
创建cutSamePlayer依赖于创建cutSameSource对象,所以需要先创建cutSameSource对象
cutSamePlayer使用完后需要调用release()释放资源
根据 模板&处理后的素材 生成实时预览的视频
/** * @param mediaItems: 媒体素材 * @param textItems: 文本素材 * @param listener: 添加播放状态回调接口 */ fun preparePlay(mediaItems: List<MediaItem>?, textItems: List<TextItem>?) //PlayerStateListener注册方法 fun registerPlayerStateListener(playerStateListener: PlayerStateListener) fun unRegisterPlayerStateListener(playerStateListener: PlayerStateListener)
//播放状态回调接口 interface PlayerStateListener { companion object { //数据准备之前状态 const val PLAYER_STATE_IDLE = 1001 //数据准备完成状态 const val PLAYER_STATE_PREPARED = 1002 //播放暂停状态 const val PLAYER_STATE_PAUSED = 1004 //正在播放状态 const val PLAYER_STATE_PLAYING = 1005 //播放错误状态 const val PLAYER_STATE_ERROR = 1006 //播放销毁状态 const val PLAYER_STATE_DESTROYED = 1007 } //首桢完成播放 fun onFirstFrameRendered() //状态变化 fun onChanged(state: Int) //播放进度 fun onPlayProgress( process: Long) //播放结束 fun onPlayEof() //播放对象错误 fun onPlayError(what: Int, extra: Int) }
val listener = object : PlayerStateListener { override fun onFirstFrameRendered() { } override fun onChanged(player: BasePlayer, state: Int) { when (state) { PlayerStateListener.PLAYER_STATE_PREPARED -> { Log.d(TAG, "PLAYER_STATE_PREPARED") cutSamePlayer?.start() } PlayerStateListener.PLAYER_STATE_PLAYING -> { //正在播放状态 } PlayerStateListener.PLAYER_STATE_ERROR -> { //播放错误状态 } PlayerStateListener.PLAYER_STATE_IDLE -> { //数据准备之前状态 } PlayerStateListener.PLAYER_STATE_PAUSED -> { //播放暂停状态 } PlayerStateListener.PLAYER_STATE_DESTROYED -> { //播放销毁状态 } } } override fun onPlayEof() { //播放结束 } override fun onPlayError(player: BasePlayer, what: Int, extra: Int) { //播放对象错误 } override fun onPlayProgress(player: BasePlayer, process: Long) { //播放进度 } } cutSamePlayer?.preparePlay(mediaItemList, textItemList)
获取模板的媒体素材,即模板的可变槽位信息
/** * 获取模版媒体素材 * cutSamePlayer.getMediaItems() */ fun getMediaItems(): ArrayList<MediaItem>?
/** * 媒体素材数据结构 * 不需业务赋值:业务不需要对extra或者CutSameSource.prepareSoure()返回的List<MediaItem>相关字段进行修改或者赋值。 * 需业务赋值 :业务需要对extra或者CutSameSource.prepareSoure()返回的List<MediaItem>相关字段进行修改或者赋值。 * 根据业务需求确认是否赋值:业务根据自身需求对extra或者CutSameSource.prepareSoure()返回的List<MediaItem>相关字段进行修改 */ data class MediaItem( // ********* 属性 ************************** var materialId //定位project segment用,必须,不需业务赋值,可从extra或者CutSameSource.prepareSoure()回调中获取。 : String, var targetStartTime // 素材槽位在模版的开始时间,必须,不需业务赋值,可从extra或者CutSameSource.prepareSoure()回调中获取。 : Long = 0, var isMutable // 素材槽位是否可变,true 表示可变,必须,不需业务赋值,可从extra或者CutSameSource.prepareSoure()回调中获取。 : Boolean, var alignMode // 视频素材对齐方式 "align_video" "align_canvas",必须,不需业务赋值,可从extra或者CutSameSource.prepareSoure()回调中获取。 : String = "", var isSubVideo // 是否副轨视频,非必须,不需业务赋值,不需业务赋值, 可从extra或者CutSameSource.prepareSoure()回调中获取。 : Boolean = false, var isReverse // 是否需要倒放,必须,不需业务赋值, 可从extra或者CutSameSource.prepareSoure()回调中获取。 : Boolean = false, var cartoonType // 漫画脸效果,非必须,不需业务赋值, 可从extra或者CutSameSource.prepareSoure()回调中获取。 : Int = 0, var width // 宽,必须,不需业务赋值,可从extra或者CutSameSource.prepareSoure()回调中获取。 : Int, var height // 高,必须,不需业务赋值,可从extra或者CutSameSource.prepareSoure()回调中获取。 : Int, var duration // 时长,必须,不需业务赋值,可从extra或者CutSameSource.prepareSoure()回调中获取。 : Long, var oriDuration // 替换素材的时长,必须,需业务赋值,在填充模版槽位信息时需要传入。 : Long = 0, var source : String = "", // 素材路径 必须,需业务赋值,在填充模版槽位信息时需要传入。 var sourceStartTime // 时间裁剪起始点,默认值为0,必须,根据业务需求确认是否赋值,extra或者CutSameSource.prepareSoure()回调中有值,如业务有视频裁剪需求,需要传入该值。 : Long = 0, var cropScale: Float = 0f,// 素材缩放,必须,根据业务需求确认是否赋值,extra或者CutSameSource.prepareSoure()回调中有值,如业务有素材缩放需求,需要传入该值。 var crop // 空间裁剪区域 必须,根据业务需求确认是否赋值,extra或者CutSameSource.prepareSoure()回调中有值,如业务有素材缩放需求,需要传入该值。 : ItemCrop = ItemCrop(0f,0f,0f,0f), /** * [TYPE_VIDEO], [TYPE_PHOTO] */ val type // 视频类型,"photo"、"video" 必须,需业务赋值,在填充模版槽位信息时需要传入。 : String = "", val mediaSrcPath // 素材原路径 必须,需业务赋值,在填充模版槽位信息时需要传入。 : String = "" val targetEndTime // 素材在模版的结束时间,必须,不需业务赋值, 可从extra或者CutSameSource.prepareSoure()回调中获取。 : Long = 0, val volume //音量 必须,根据业务需求确认是否赋值,extra或者CutSameSource.prepareSoure()回调中有值,如业务有音量调节需求,需要传入该值。 : Float = 0f, val relation_video_group // 槽位分组,必须,不需业务赋值,可从extra或者CutSameSource.prepareSoure()回调中获取。 : String = "" )
val cutSamePlayer: CutSamePlayer? = CutSameSolution.createCutSamePlayer(videoSurfaceView, sourceUrl) val mediaItems = cutSamePlayer?.getMediaItems()
支持替换媒体素材功能,即替换槽位素材
/** * 更新文本 * @param materialId: 素材ID * @param mediaItem: 媒体素材 */ fun updateMedia(materialId: String, mediaItem: MediaItem)
val cutSamePlayer: CutSamePlayer? = CutSameSolution.createCutSamePlayer(videoSurfaceView, sourceUrl) //repalcedMediaItem为通过素材替换页选择的素材 cutSamePlayer?.updateMedia(materialId,repalcedMediaItem)
获取模版文本素材
/** * 获取模版文本素材 */ fun getTextItems(): ArrayList<TextItem>?
/** * 模版文本素材数据结构 */ data class TextItem( /** * 时长 */ val duration: Long = 0, /** * 是否可被更改 */ val mutable: Boolean = false, /** * 素材ID */ val materialId: String = "", /** * 旋转角度 */ val rotation: Double = 0.0, /** * 展示的起始时间 */ val targetStartTime: Long = 0L, /** * 文本内容 */ var text: String = "" )
val cutSamePlayer: CutSamePlayer? = CutSameSolution.createCutSamePlayer(videoSurfaceView, sourceUrl) val textItems = cutSamePlayer?.getTextItems()
支持编辑模版文本功能
/** * 更新文本 * @param materialId: 素材ID * @param text: 要修改的文本 */ fun updateText(materialId: String, text: String)
val cutSamePlayer: CutSamePlayer? = CutSameSolution.createCutSamePlayer(videoSurfaceView, sourceUrl) val textItems = cutSamePlayer?.updateText(materialId, "修改的文本")
支持控制播放,如开始、暂停、以及Seek到指定时间
//开始播放 fun start() //暂停播放 fun pause() /** * 拖动到指定时间播放 * @param position: 拖动到的位置 单位:ms * @param isAutoPlay: 是否自动播放 */ fun seekTo(position: Int, isAutoPlay: Boolean)
//调用播放控制接口也会触发该接口的回调 interface PlayerStateListener { companion object { //数据准备之前状态 const val PLAYER_STATE_IDLE = 1001 //数据准备完成状态 const val PLAYER_STATE_PREPARED = 1002 //播放暂停状态 const val PLAYER_STATE_PAUSED = 1004 //正在播放状态 const val PLAYER_STATE_PLAYING = 1005 //播放错误状态 const val PLAYER_STATE_ERROR = 1006 //播放销毁状态 const val PLAYER_STATE_DESTROYED = 1007 } //首桢完成播放 fun onFirstFrameRendered() //状态变化 fun onChanged(player: BasePlayer, state: Int) //播放进度 fun onPlayProgress(player: BasePlayer, process: Long) //播放结束 fun onPlayEof() //播放错误 fun onPlayError(player: BasePlayer, what: Int, extra: Int)
val cutSamePlayer: CutSamePlayer? = CutSameSolution.createCutSamePlayer(videoSurfaceView, sourceUrl) cutSamePlayer?.seekTo(timePos, true) cutSamePlayer?.start() cutSamePlayer?.pause()
/** * @param materialId: 素材ID * @param volume: 音量值 0..1 */ fun setVolume(materialId: String, volume: Float)
volumeSliderView.setOnSliderChangeListener(object : OnSliderChangeListener() { override fun onChange(value: Int) { //.... } override fun onBegin(value: Int) { //.... } override fun onFreeze(value: Int) { selectMediaItem?.let { //设置音量 cutSamePlayer?.setVolume(it.materialId, value.toFloat() / 100) } } })
/** * @param materialId: 素材ID * @return Float: 音量值 */ fun getVolume(materialId: String): Float
//音量view显示的时候获取音量 private fun showSlotVolumeLayout() { if (volumeLayout.visibility == View.GONE) { selectMediaItem?.let { //获取音量 val volume = cutSamePlayer?.getVolume(it.materialId)?.times(100)?.toInt() ?: 0 volumeSliderView.currPosition = volume } volumeLayout.visibility = View.VISIBLE } }
interface ICutSameAudio { companion object { const val SUCCESS = 0 /** * 输入参数不符合条件 */ const val PARAMS_ERROR = -1 /** * 无效的音频文件信息 */ const val AUDIO_INFO_INVALID = -2 /** * 无效音量值 */ const val INVALID_VOLUME = -1.0f } /** * 设置模板自定义音频 * @param path 音频文件本地路径 * @param audioName 音频名称 可选参数 * @param fromTimeMs 从该音频文件哪个时间点开始播放,例如:原音频文件有3分钟,180s,但是模板音频只需要10s, * 这是我希望从选取副歌部分可能是100s-110s,那么该参数应该传 100 * 1000,单位是毫秒,如果希望从头开始就直接设置0. * * @return 错误码 */ fun setCustomAudio(path: String, audioName: String = "", fromTimeMs: Long): Int /** * 获取模板音频的起始时间 * @return 单位ms */ fun getTemplateAudioStartTime(): Long /** * 获取模板音频的总时长 * @return 单位ms */ fun getTemplateAudioDuration(): Long /** * 重置成原模板音频 */ fun resetAudio(): Int /** * 设置音量,如果有定制音乐,只设置定制音乐轨音量,如果无定制音乐轨,则设置原始音乐轨轨道 * @param volume 0.0 - 1.0 * * @return 错误码 */ fun setAudioVolume(volume: Float): Int /** * 获取当前音量,首先获取通过setAudioVolume(volume)接口设置过的音量, * 未调用过首先获取通过setAudioVolume(volume), 会先获取自定义音频的音量, * 未设置自定义音频时,会获取模板原始音频音量,优先获取首个关键帧的音量,其次是原始音频音量 * * @return 对应音量值,如果找不到则返回 * @see INVALID_VOLUME */ fun getAudioVolume(): Float }
// 从 CutSamplyer 获取 音频操作接口 val cutSameAudio: ICutSameAudio = cutSamePlayer.getAudioManager() //替换原来背景音乐 cutsameAudio.setCustomAudio("/sdcard/Android/data/[pacakge]/music.mp3", "audioName", 0L) //设置音量 0.0~0.1 float cutsameAudio.setAudioVolume(0.8f) //还原原来音频 cutsameAudio.resetAudio() //获取当前音量 cutsameAudio.getAudioVolume()
将重演后的视频导出到本地
/** * 导出视频接口 * @param outFilePath: 导出路径 * @param compileParam: 导出编码参数 * @param listener: 导出状态监听 */ fun compileNLEModel(outFilePath: String, compileParam: CompileParam?, listener: CompileListener) /** * 取消导出视频接口 */ fun cancelCompile()
/** * 导出编码参数数据结构 * @param resolution: 分辨率 * @param supportHwEncoder: true:硬编码 false 软编码 * @param bps: 视频码率 * @param fps: 视频帧率 * @param gopSize: gop * @param swMaxRate: swMaxRate * @param swCRF: swCRF * @param bitrateMode: bitrateMode */ data class CompileParam( val resolution: ExportResolution, val supportHwEncoder: Boolean, val bps: Int, val fps: Int, val gopSize: Int, val swMaxRate: Long, val swCRF: Int, val bitrateMode: VEVideoEncodeSettings.ENCODE_BITRATE_MODE ) : Parcelable enum class ExportResolution(val label: String, val width: Int, val height: Int, val level: Int) { V_4K("4K", 3840, 2160, 4000), V_2K("2K", 2560, 1440, 2000), V_1080P("1080p", 1920, 1080, 1080), V_720P("720p", 1280, 720, 720), V_480P("480p", 858, 480, 480) }
val param = createCompileParam() val outputVideoPath = "/storage/emulated/0/相机/20210528143142.mp4" cutSamePlayer?.compileNLEModel(outputVideoPath, param, object : CompileListener { override fun onCompileDone() { Log.d(TAG, "onCompileDone") //导出完成 } override fun onCompileProgress(progress: Float) { //导出进度 } override fun onCompileError( error: Int, ext: Int, f: Float, msg: String? ) { //导出失败 } })
获取某个时间点的截图
/** * 更新文本素材 * @param timeStamps: 要获取的时间点,单位毫秒 * @param width: 要获取的帧宽度 * @param height: 要获取的帧高度 * @param listener: 取帧回调接口 */ fun getVideoFrameWithTime(timeStamps: IntArray, width: Int, height: Int, listener: GetImageListener)
val cutSamePlayer: CutSamePlayer? = CutSameSolution.createCutSamePlayer(videoSurfaceView, sourceUrl) cutSamePlayer?.getVideoFrameWithTime(timeStamps, width,height, object :GetImageListener{ override fun onGetImageData( bytes: ByteArray?, pts: Int, width: Int, height: Int, score: Float ) { if (bytes != null) { val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) bmp.copyPixelsFromBuffer(ByteBuffer.wrap(bytes)) listener?.frameBitmap(pts.toString(), bmp) } else { cutSamePlayer?.cancelGetVideoFrames() // 抽帧结束后需要cancel,不然会停留在抽帧状态 } } })