# TAVMedia

TAVMedia 是一个跨平台的音视频编辑框架，支持在移动端、桌面端、浏览器端和服务器端使用。

# Web 版使用指南

## 浏览器支持
TAVMedia 目前支持 Chrome 75+, Safari 12+.
## 安装

在 Web 工程中使用 TAVMedia，您可以直接使用 NPM 将 TAVMedia 安装到您的项目中
```shell
npm install tav-media
```

## 初始化 tav-media

```typescript
import { initializeWasm, TAVWasmOptions } from 'tav-media';

const env: TAVWasmOptions = {
  /**
   * 可选，指定 wasm 文件在服务端的路径。如果不指定，则使用 `./tav-media-wasm.wasm`
   */
  locateFile: wasmFileName => `/node_modules/tav-media/bin/wasm/${wasmFileName}`,
  /**
   * 可选，后续资源的 path 为相对路径时，会自动添加此前缀
   */
  baseUrl: '/',
};

initializeWasm(env).then(async () => {
  // 调用业务初始化代码
});

```

# License 指引

## License 鉴权
在 TAVMedia SDK 中提供了鉴权的对应的 API ，可以参考使用：

```typescript
/**
 * 验证授权信息
 */
async function authorize() {
  const authResult = await TAVLicense.Auth(
    // License url 或者 license 内容的 ArrayBuffer
    'assets/tav_media.license',
    // AppId
    'your_appid_here',
    // License secret
    'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
  );
  if (authResult !== OAuthErrorCode.OK) {
    // 根据验证结果进行进一步处理
    console.warn('授权失败，使用 TAVMedia 水印试用版');
  }
}

initializeWasm(env).then(async () => {
  // 加载 TAVMedia wasm 后验证 license
  await authorize();
});
```

# 基础功能
## 使用 TAVMedia 渲染视频

TAVMedia Web 版封装了 `TAVMediaView` 类， 封装了 `TAVSurface`, `TAVVideoReader`, `TAVAudioReader` 的创建和调用过程，
提供了适合 Web 播放使用的 API。视频图像渲染到指定的 canvas，**音频会直接通过浏览器输出，暂时不支持读取每帧的音频合成数据**。

```typescript
// 创建一个 MovieClip 对象
const ghost = await MovieAsset.MakeFromPath('assets/ghost.mp4');
const movie = await MovieClip.MakeFromAsset(ghost);
movie.duration = ghost.duration;

// 创建一个 TAVMediaView，将图像渲染到 id 为 stage 的 canvas
const view = TAVMediaView.MakeFromHtmlCanvas('#stage');

// 播放视频
await view.setMedia(movie);
view.play();
```

## 同时播放多个媒体

通过将多个媒体添加到 Composition 的方式，可以在场景中同时播放多个视频和特效。例如下面的视频转场的例子。

```typescript
// 创建主时间轴
const root = new Composition();
root.width = 1280;
root.height = 720;
root.duration = 3_000_000;

// 添加第一个视频
const ghost = await MovieAsset.MakeFromPath('video-640x360.mp4');
const movie1 = await MovieClip.MakeFromAsset(ghost);
movie1.duration = ghost.duration;
movie1.startTime = 0;

// 添加第二个视频
const dog = await MovieAsset.MakeFromPath('video-640x360-2.mp4');
const movie2 = await MovieClip.MakeFromAsset(dog);
movie2.duration = dog.duration;
movie2.startTime = 800_000;

// 添加转场
const zcAsset = await PAGAsset.MakeFromPath('zc.pag');
const zc = await PAGEffect.MakeFromAsset(zcAsset);
zc.startTime = 800_000;
zc.duration = zcAsset.duration;
// 将两个视频作为转场效果的输入，在转场播放过程中，将根据设计师
// 指定的方式展示两个视频切换的效果。
zc.addInput(movie1);
zc.addInput(movie2);

// 添加到主时间轴
root.addClip(movie1);
root.addClip(movie2);
root.addClip(zc);

// 播放
await view.setMedia(root);
view.play();
```

# 层级结构

# 显示控制

## Matrix 和 CropRect 控制位移缩放旋转裁切
```typescript
const asset = await MovieAsset.MakeFromPath('video-640x360.mp4');
const clip = await MovieClip.MakeFromAsset(asset);
clip.duration = 3_000_000;

const matrix = new Matrix();
matrix.postTranslate(-asset.width / 2, -asset.height / 2);
matrix.postScale(2, 2);
matrix.postTranslate(asset.width / 2, asset.height / 2);

const cropRect = Rect.MakeXYWH(asset.width / 4, asset.height / 4, asset.width / 2, asset.height / 2);
clip.matrix = matrix;
clip.cropRect = cropRect;
```
## Opacity
opacity是不透明度，通过设置opacity可以控制Movie的透明度。取值范围是0.0-1.0。0是完全透明，1是完全不透明。

```typescript

const clip = await MovieClip.MakeFromAsset(asset);
clip.opacity = 0.5;
```

# 时间控制

## 通过 Clip 和 Composition 排布时间轴
```typescript
const ghost = await MovieAsset.MakeFromPath('video-640x360.mp4');
const movie1 = await MovieClip.MakeFromAsset(ghost);
movie1.startTime = 1_000_000;
movie1.duration = 4_000_000;

// 创建一个分辨率为 720 * 1280 的Composition
// contentStartTime 为 1_000_000，表示从视频的第1秒开始播放
// contentDuration 为 10_000_000，表示播放 10 秒
const composition = await Composition.Make(720, 1280, 2_000_000, 10_000_000);
composition.duration = 10_000_000;
composition.addClip(movie1);
```

## 通过 MovieClip 裁剪资源片段
```typescript
const ghost = await MovieAsset.MakeFromPath('video-640x360.mp4');
const movie1 = await MovieClip.MakeFromAsset(ghost, 1_000_000, 4_000_000);
movie1.startTime = 0;
movie1.duration = 4_000_000;
```

## 变速
```typescript
// 通过 Movie 直接变速
const totalDuration = 10_000_000;
const asset = await MovieAsset.MakeFromPath('video-640x360.mp4');
const movie = await MovieClip.MakeFromAsset(asset, 0, totalDuration);
movie.startTime = 0;
movie.duration = totalDuration / 2;

// 通过 Composition 进行变速
const asset1 = await MovieAsset.MakeFromPath('video-640x360.mp4');
const movie1 = await MovieClip.MakeFromAsset(asset1, 0, totalDuration);
movie1.startTime = 0;
movie1.duration = totalDuration;
const composition = await Composition.Make(640, 360, 0, totalDuration);
composition.addClip(movie1);
composition.startTime = 0;
composition.duration = totalDuration / 2;
```

## 定格

```typescript
// 通过 Movie 定格
const totalDuration = 10_000_000;
const asset = await MovieAsset.MakeFromPath('video-640x360.mp4');
const movie = await MovieClip.MakeFromAsset(asset, 1_000_000, 0);
movie.startTime = 0;
movie.duration = totalDuration;

// 通过 Composition 定格
const asset1 = await MovieAsset.MakeFromPath('video-640x360.mp4');
const movie1 = await MovieClip.MakeFromAsset(asset1, 0, totalDuration);
movie1.startTime = 0;
movie1.duration = totalDuration;
const composition = await Composition.Make(640, 360, 1_000_000, 0);
composition.addClip(movie1);
composition.startTime = 0;
composition.duration = totalDuration;
```

# 音频播放
## TAVMedia的音频播放

```typescript
const totalDuration = 10_000_000;
const path = 'hoaprox.mp3';
const asset = await AudioAsset.MakeFromPath(path);
const audio = await AudioClip.MakeFromAsset(asset, 0, totalDuration);
audio.duration = totalDuration;
const effect = await AudioVolumeEffect.MakeFIFOEffect(audio, 1.0, 3_000_000, 3_000_000);
effect.startTime = 0;
effect.duration = totalDuration;
```

# 添加效果

## TransformEffect

```typescript
const asset1 = await MovieAsset.MakeFromPath('video-640x360.mp4');
const movie1 = await MovieClip.MakeFromAsset(asset1);
movie1.startTime = 0;
movie1.duration = asset1.duration;

const transformEffect = TransformEffect.MakeTransformEffect();
const keyframes: NoBlankArray<Keyframe> = [
  TAVKeyframe.MakeLinear(0, 1_000_000, 0, 90),
];
transformEffect.transform2D = {
  rotation: TAVProperty.MakeAnimatableProperty(keyframes),
}
transformEffect.addInput(movie1);
```

## TAVColorTuningEffect

```typescript
const asset1 = await MovieAsset.MakeFromPath('video-640x360.mp4');
const movie1 = await MovieClip.MakeFromAsset(asset1);
movie1.startTime = 0;
movie1.duration = asset1.duration;

const colorTuningEffect = ColorTuningEffect.MakeColorTuningEffect();
const keyframes: NoBlankArray<Keyframe> = [
  TAVKeyframe.MakeLinear(0, 1_000_000, -50, 50),
];

// 设置饱和度
colorTuningEffect.colorTuning = {
  saturation: TAVProperty.MakeAnimatableProperty(keyframes),
}
colorTuningEffect.addInput(movie1);
```

## LUTEffect

```typescript
const asset1 = await MovieAsset.MakeFromPath('video-640x360.mp4');
const movie1 = await MovieClip.MakeFromAsset(asset1);
movie1.startTime = 0;
movie1.duration = asset1.duration;

const lutEffect = await LUTEffect.MakeFromPath('lut.png');
lutEffect.startTime = 0;
lutEffect.duration = 1_000_000;
lutEffect.strength = 0.5;
lutEffect.addInput(movie1);
```

## ChromaMatting

```typescript
const asset1 = await MovieAsset.MakeFromPath('video-640x360.mp4');
const movie1 = await MovieClip.MakeFromAsset(asset1);
movie1.startTime = 0;
movie1.duration = asset1.duration;

const chromaMattingEffect = ChromaMattingEffect.Make();
chromaMattingEffect.chromaMattingConfig = {
  intensity: 0.2,
  shadow: 0.5,
  selectedColor: {
    red: 0,
    green: 255,
    blue: 0,
    alpha: 255,
  },
}
chromaMattingEffect.startTime = 0;
chromaMattingEffect.duration = 1_000_000;
chromaMattingEffect.addInput(movie1);
```

## PAGEffect

```typescript
const asset1 = await MovieAsset.MakeFromPath('video-640x360.mp4');
const movie1 = await MovieClip.MakeFromAsset(asset1);
movie1.startTime = 0;
movie1.duration = asset1.duration;

const pagAsset = await PAGAsset.MakeFromPath('fw.pag');
const pagEffect = await PAGEffect.MakeFromAsset(pagAsset);
// numImgs 表示该 PAG File 最大支持的替换图层数量
const numImgs = pagEffect.numImages;

// 添加 movie1 作为 effect 的输入, 
// scaleMode 可以设置如何缩放 movie1 以适应 PAG 图层的大小
// editableIndex 表示 PAG File 中的第几个可替换图层
pagEffect.addInput(movie1, ScaleMode.LetterBox, 0);
pagEffect.startTime = 0;
// 这边可以根据需求设置成任意时长，例子中设置成文件时长
pagEffect.duration = pagAsset.duration;
```

```typescript
const movieDuration = 3_000_000;
const asset1 = await MovieAsset.MakeFromPath('1.mp4');
const movie1 = await MovieClip.MakeFromAsset(asset1);
movie1.duration = movieDuration;

const asset2 = await MovieAsset.MakeFromPath('2.mp4');
const movie2 = await MovieClip.MakeFromAsset(asset2);
movie2.duration = movieDuration;

const pagAsset = await PAGAsset.MakeFromPath('zc.pag');
const pagEffect = await PAGEffect.MakeFromAsset(pagAsset);
const numImgs = pagEffect.numImages;
// 分别设置 movie1 和 movie2 替换 PAG file 中的第一个替换图层和第二个替换图层
// 替换哪两个替换图层需要根据 PAG 文件来决定，通常情况下 input0 对应 editableIndex0，input1 对应 editableIndex1
// 也可以通过 `addInput` 的 `editableIndex` 参数来指定
pagEffect.addInput(movie1);
pagEffect.addInput(movie2);
// 这边可以根据需求设置成任意时长，例子中设置成文件时长
pagEffect.duration = pagAsset.duration;
// 设置转场和 movie2 的开始时间为 movie1 的时长减去 PAG File 的时长，
// 这样 PAG Effect 能够将效果作用到 movie1 和 movie2 之间
movie2.startTime = movie1.duration - pagEffect.duration;
pagEffect.startTime = movie2.startTime;
```