movie-editor-lib
背景
原有的数字人生成方式,是通过drml去驱动数字人,然后生成数字人视频,但是这种方式局限性比较大,一是只涉及数字人,二是drml驱动是一种线编形式的视频编辑方式
那么非线编是什么呢?非线编全程为非线性编辑,即剪切、复制、粘贴素材无须在存储介质上重新安排他们;非线性的主要目标是提供对原素材任意部分的随机存取、修改和处理
最开始是服务型非线编平台有需求,服务型是云端数字人方案,数字人为远端视频,演艺型直播平台现在的方案是本地驱动ue客户端
视频编辑器核心主要分为两部分,一是非线编轴操作,二是视频预览界面
使用
<MovieEditorKit
className="demo-wrapper"
ref={editorRef}
aspect={resolution[0] / resolution[1]}
tracks={tracks as TrackType[]}
onFocus={handleFocus}
onChange={handleChange}
onSave={handleSave}
>
<UEPreview>
<DigitalHumanView className="demo-streaming" />
</UEPreview>
<ZoomController />
<TimeLine />
<BlockLine />
</MovieEditorKit>
1. MovieEditorKit
参数名 | 类型 | 备注 |
---|---|---|
ref | MovieEditorHandler | 编辑器方法暴露 |
aspect | Number | 画幅比例,比如16/9 |
tracks | TrackType[] | 核心数据结构 |
onFocus | (clip?: TrackClipType) => void | 选中事件 |
onChange | (tracks: TrackType[], duration: number) => void | tracks变更 |
onSave | (tracks: TrackType[], duration: number) => void | 触发保存事件(ctrl+s) |
onEnd | () => void | 预览播放完成 |
MovieEditorKit这一层是整个视频编辑器的核心,所有数据的通信以及处理都在这一层,它给外部暴露了编辑器的相关方法MovieEditorHandler
方法名 | 类型 | 备注 |
---|---|---|
set | (obj: Partial) => void | 更新某个clip |
update | (tracks: TrackType[]) => void; | 更新整个tracks |
prepare | (obj: Partial) => void; | 拖拽新增某个clip前调用 |
reset | () => void; | 拖拽新增某个clip后调用 |
seek | (time: number) => void; | 跳转到预览的某一时间 |
play | () => void; | 开始预览播放 |
pause | () => void; | 暂停预览播放 |
但是视频编辑器的相关方法,除了外部调用以为,还有内部调用,比如demo里的组件,可以通过开发自定义组件,将自定义组件放在
MovieEditorKit内部,然后通过useContext获取相关方法
方法名 | 类型 | 备注 |
---|---|---|
tracks | TrackType[] | 核心数据结构 |
aspect | Number | 画幅比例,比如16/9 |
duration | number | 视频时长 |
focusId | number | 命中的clipId |
focusClip | TrackClipType | 命中的clip |
focusBindClips | Set | 命中的clip的绑定clips |
currentTime | number | 当前预览播放到的时间 |
playing | boolean | 当前预览播放状态 |
zoomConfig | ZoomConfig {zoom?: number; minZoom?: number; maxZoom?: number; onZoomChange?: ((zoom: number |((prev: number)=>number)) => void); } | 当前缩放比以及轴状态 |
virtualClip | VirtualClipType (TrackClipType & {point: Point; target: string; }) | 拖动或跳轴时,处于动态时的clip |
onClipChange | (clip?: Partial, isFocus?: boolean) => void; | clip触发变更 |
onTrackChange | (track: TrackType) => void; | tracks触发变更 |
dispatch | React.Dispatch | 其余数据状态变化触发 |
通过以上暴露的方法,基本就能对视频编辑器进行完整的操作
下面我们就开始讲最核心的部分,也就是数据结构,视频编辑器操作处理的数据
2. Tracks
通过ts的定义,可以看到,我们的tracks定义为TrackType,本质是我们提供的各种类型的Track
export type TrackType = VideoTrack | ImageTrack | AudioTrack | TextTrack | DigitalHumanTrack | FragmentTrack;
目前,我们提供的Track的类型为Video视频、Image图片、Audio音频、Text文本、DigitalHuman数字人、Fragment片段
具体的结构,我们直接看定义
// track
export interface MovieTrack {
name: string;
id: string;
color?: string;
// 是否磁吸,连续
attraction?: boolean;
isMainTrack?: boolean;
masterId?: string;
associatedId?: string;
// 操作
operations: Partial<MovieTrackOperation>;
}
export interface DigitalHumanTrack extends MovieTrack {
type: ElementType.DIGITAL_HUMAN;
digitalhuman: DigitalHumanType,
position: Position;
clips: DigitalHumanClip[];
}
export interface VideoTrack extends MovieTrack {
type: ElementType.VIDEO;
position: Position;
clips: (VideoClip | ImageClip)[];
}
export interface ImageTrack extends MovieTrack {
type: ElementType.IMAGE;
position: Position;
clips: ImageClip[];
}
export interface AudioTrack extends MovieTrack {
type: ElementType.AUDIO;
clips: AudioClip[];
}
export interface TextTrack extends MovieTrack {
type: ElementType.TEXT;
position: Position;
source: Omit<TextStyle, 'argument'>;
clips: TextClip[];
}
export interface FragmentTrack extends MovieTrack {
type: ElementType.FRAGMENT;
position: Position;
clips: TrackClipType[];
}
这里可以看到,这里主要定义了Track的类型,并且每个Track内的数据结构也不一样
通过MovieTrack可以看到,track本身的一些功能
-
attraction:是否吸附连续轴
-
isMainTrack:是否主轴,目前还没用,之后做一些辅助线需要
-
masterId、associatedId: 关联轴
track上的position、operations不是表示track的位置信息和操作,是这个轴上的clip的默认信息,就是当clip添加的时候,如果没有设置相关的信息,那么就会从track上获取
这里讲一下比较复杂的FragmentTrack,它表达一种非常特殊的track类型,可以拖入所有类型的clip,即不进行track类型的限制,并且两种特殊类型的clip也只能在这个track下使用,具体的clip下面讲解
export type TrackClipType = DigitalHumanClip | VideoClip | ImageClip | TextClip | AudioClip | FragmentClip | CustomClip;
以上是所有的clip类型,基础的clip属性有
export interface MovieClip {
// 标识符
id: string;
// 来自轴id
from: string;
// 起始时间
offset: number;
// 素材起始时间
in: number;
// 素材结束时间
out: number;
// 素材原本时长
duration: number;
// 预览位置信息
position: Position;
// 是否固定起始位置
offsetFixed?: boolean;
// 素材内容
source: {
// 素材地址
url: string;
// 预览图
thumbnail?: string;
};
// 关联轴-主轴id
masterId?: string;
// 关联轴-副轴id
associatedId?: string;
// 关联轴-clip预览
associate?: TrackClipType[];
// 操作
operations: Partial<MovieTrackOperation>;
}
export interface DigitalHumanClip extends Omit<MovieClip, 'source'> {
type: ElementType.DIGITAL_HUMAN;
digitalhuman: DigitalHumanType;
source: DigitalHumanSource;
}
export interface VideoClip extends MovieClip {
type: ElementType.VIDEO;
}
export interface ImageClip extends MovieClip {
type: ElementType.IMAGE;
}
export interface AudioClip extends Omit<MovieClip, 'position'> {
type: ElementType.AUDIO;
}
export interface TextClip extends Omit<MovieClip, 'source'> {
type: ElementType.TEXT;
source: {
content: ElementStructure[];
style: React.CSSProperties;
},
}
export interface FragmentClip extends Omit<MovieClip, 'source'> {
type: ElementType.FRAGMENT;
source: {
config: TrackType[];
// 预览图
thumbnail?: string;
title?: string;
duration?: number;
},
}
export interface CustomClip extends Omit<MovieClip, 'source'> {
type: ElementType.CUSTOM;
source: any;
onPreview: (clip: CustomClip) => (() => void);
onRender: (clip: CustomClip) => ReactNode | void;
}
3. UE驱动
端渲染方案中,比较大的区别是在于Preview预览的部分,这里为了保持双方的独立性,新开发了UEPreview组件,在使用的时候,可以根据实际情况进行使用
主要区别在于驱动方式,一个是组件的渲染,一个是去驱动ue
4. 皮肤
--theme-bg: #2D2E2E;
--preview-bg: #000;
--preview-content-bg: #D9DEE4;
--text-color: #F0F0F0;
--control-slider: #BDBDBD;
--control-slider-solid: #00CCCC;
--control-bg: #3D3D3D;
--control-color: #ABABAB;;
--timeline-pointer-color: #fff;
--timeline-pointer-width: 1.5px;
--timeline-pointer-size: 8px;
--track-title-color: #666666;
--track-title-icon-color: #F0F0F0;
--track-title-width: 48px;
--track-title-postion: center;
--track-ruler-height: 30px;
--track-margin: 4px;
--track-height: 20px;
--track-bg: #1D1E1F;
--track-disabled-bg: none;
--track-enabled-bg: rgba(86, 98, 239, .2);
--track-preview-height: 80px;
--track-block-width: 160px;
--track-block-height: 90px;
--track-block-bg: #1D1E1F;
--clip-bg: #848DF1;
--clip-shadow: normal;
--clip-border: 1px solid #FFFFFF;
--clip-border-radius: 4px;
--clip-focus-bg: normal;
--clip-btn-bg: #FFFFFF;
--clip-btn-length: 2px;
--clip-btn-content: '';
--clip-menu-hover-bg: #3D3D3D;
--clip-menu-hover-color: #FAFAFA;
--preview-edit-border-width: 1px;
--preview-edit-border: var(--preview-edit-border-width) solid #fff;
--editor-btn-width: 10px;
--editor-btn-color: #fff;
--editor-btn-radius: 50%;