1.0.0-beta.6 • Published 2 years ago

movie-editor-lib v1.0.0-beta.6

Weekly downloads
-
License
MIT
Repository
-
Last release
2 years ago

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

参数名类型备注
refMovieEditorHandler编辑器方法暴露
aspectNumber画幅比例,比如16/9
tracksTrackType[]核心数据结构
onFocus(clip?: TrackClipType) => void选中事件
onChange(tracks: TrackType[], duration: number) => voidtracks变更
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获取相关方法

方法名类型备注
tracksTrackType[]核心数据结构
aspectNumber画幅比例,比如16/9
durationnumber视频时长
focusIdnumber命中的clipId
focusClipTrackClipType命中的clip
focusBindClipsSet命中的clip的绑定clips
currentTimenumber当前预览播放到的时间
playingboolean当前预览播放状态
zoomConfigZoomConfig {zoom?: number; minZoom?: number; maxZoom?: number; onZoomChange?: ((zoom: number |((prev: number)=>number)) => void); }当前缩放比以及轴状态
virtualClipVirtualClipType (TrackClipType & {point: Point; target: string; })拖动或跳轴时,处于动态时的clip
onClipChange(clip?: Partial, isFocus?: boolean) => void;clip触发变更
onTrackChange(track: TrackType) => void;tracks触发变更
dispatchReact.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%;