Remotion LabRemotion Lab
建構應用在 Angular 中使用 Remotion

在 Angular 中使用 Remotion

學習如何將 Remotion Player 整合到 Angular 應用程式中,包括 React 包裝器組件和 TypeScript 設定

本指南說明如何將 Remotion 整合到 Angular 專案中。

安裝必要套件

安裝 Remotion 和必要的依賴項:

使用 npm
npm i remotion @remotion/player @remotion/cli @remotion/zod-types react react-dom zod
npm i --save-dev @types/react @types/react-dom
使用 pnpm
pnpm i remotion @remotion/player @remotion/cli @remotion/zod-types react react-dom zod
pnpm i --dev @types/react @types/react-dom
使用 Yarn
yarn add remotion @remotion/player @remotion/cli @remotion/zod-types react react-dom zod
yarn add --dev @types/react @types/react-dom
使用 Bun
bun i remotion @remotion/player @remotion/cli @remotion/zod-types react react-dom zod
bun --dev @types/react @types/react-dom

注意:Bun 作為執行時大部分受到支援。在此閱讀更多

建立 Remotion 資料夾

為了更好地分離關注點,建立一個資料夾來存放你的 Remotion 檔案:

src/app/remotion

將你的 Remotion 專案或入門範本(例如 HelloWorld)的內容複製到這個新資料夾中。這有助於將 Remotion 相關檔案與其他 Angular 程式碼分開。

複製 remotion.config.ts

remotion.config.ts 檔案複製到 Angular 專案的根目錄,放置在與 package.json 相同的層級。

remotion.config.ts
import { Config } from '@remotion/cli/config';
 
Config.setVideoImageFormat('jpeg');
Config.setOverwriteOutput(true);

此設定檔案對於 Remotion 識別和編譯你的專案設定是必要的。

為 JSX 配置 TypeScript

要在 Angular 中啟用 JSX 支援,請在 tsconfig.jsoncompilerOptions 下設定 "jsx": "react"

tsconfig.json
{
  "compilerOptions": {
    "jsx": "react",
    "skipLibCheck": true
  }
}

注意:同時建議設定 "skipLibCheck": true 以防止與某些函式庫類型的相容性問題。

為 Angular 建立 React 包裝器組件

要在 Angular 中嵌入 Remotion 組件,需要建立一個包裝器組件

  1. 在你的 remotion 資料夾中,建立一個名為 PlayerViewWrapper.tsx 的檔案。
  2. 確保每個 .tsx 檔案在頂部明確匯入 React:
PlayerViewWrapper.tsx
import {
  AfterViewInit,
  Component,
  effect,
  ElementRef,
  EventEmitter,
  Input,
  OnDestroy,
  Output,
  signal,
  Signal,
  ViewChild,
  WritableSignal,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import React from 'react';
import { createRoot, Root } from 'react-dom/client';
import { PlayerRef } from '@remotion/player';
import { myCompSchema, PlayerView } from './PlayerView';
import { z } from 'zod';
 
const rootDomID: string = 'reactCounterWrapperId';
 
@Component({
  selector: 'app-player-view',
  standalone: true,
  imports: [CommonModule, RouterOutlet],
  template: `<div id="${rootDomID}" #reactWrapper></div>`,
})
export class PlayerViewWrapper implements AfterViewInit, OnDestroy {
  @ViewChild(rootDomID, { static: false })
  containerRef: ElementRef | undefined;
 
  @Input({ required: true })
  data: Signal<z.infer<typeof myCompSchema>> = signal({
    titleText: '歡迎使用 Remotion',
    titleColor: '#000000',
    logoColor1: '#91EAE4',
    logoColor2: '#86A8E7',
  });
 
  @Output() onPaused = new EventEmitter<void>();
 
  playerRef: WritableSignal<PlayerRef | undefined> = signal(undefined);
 
  private root?: Root;
 
  constructor() {
    effect(() => {
      this.render();
    });
  }
 
  ngAfterViewInit() {
    this.root = createRoot(this.getRootDomNode());
    this.render();
    this.playerRef()?.play();
  }
 
  ngOnDestroy(): void {
    this.root?.unmount();
  }
 
  private getRootDomNode() {
    if (!this.containerRef || !this.containerRef.nativeElement) {
      throw new Error('Cannot get root element. This should not happen.');
    }
    return this.containerRef.nativeElement;
  }
 
  protected render() {
    if (!this.containerRef || !this.containerRef.nativeElement) {
      return;
    }
 
    this.root?.render(
      React.createElement(PlayerView, {
        ref: (ref: any) => {
          this.playerRef.set(ref?.playerRef);
        },
        data: this.data(),
        onPaused: () => this.onPaused.emit(),
      })
    );
  }
}

此包裝器組件將作為 Angular 和 Remotion React 組件之間的橋樑。

你也可以傳遞 EventEmitter 而不是 Signal。

為 Remotion Player 建立包裝器

  1. 在你的 remotion 資料夾中,建立一個名為 PlayerView.tsx 的檔案。
  2. 確保每個 .tsx 檔案在頂部明確匯入 React:
PlayerView.tsx
import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
import { Player, type PlayerRef } from '@remotion/player';
import { HelloWorld } from './HelloWorld';
import { z } from 'zod';
 
export const myCompSchema = z.object({
  titleText: z.string(),
  titleColor: z.string(),
  logoColor1: z.string(),
  logoColor2: z.string(),
});
 
export const PlayerView = forwardRef<
  { playerRef: PlayerRef | null },
  {
    data: z.infer<typeof myCompSchema>;
    onPaused?: () => void;
  }
>((props, ref) => {
  const playerRef = useRef<PlayerRef>(null);
 
  useEffect(() => {
    if (playerRef.current) {
      // 當播放器暫停時新增回調
      playerRef.current.addEventListener('pause', () => {
        props.onPaused?.();
      });
    }
  }, []);
 
  useImperativeHandle(ref, () => ({
    get playerRef() {
      return playerRef.current;
    },
  }));
 
  return (
    <Player
      ref={playerRef}
      component={HelloWorld}
      inputProps={props.data}
      durationInFrames={150}
      fps={30}
      compositionWidth={1920}
      compositionHeight={1080}
      style={{ width: '100%' }}
    />
  );
});

在 Angular 模板中使用組件

在你的 Angular 模板中使用包裝器組件:

app.component.ts
import { Component, signal } from '@angular/core';
import { PlayerViewWrapper } from './remotion/PlayerViewWrapper';
import { z } from 'zod';
import { myCompSchema } from './remotion/PlayerView';
 
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [PlayerViewWrapper],
  template: `
    <div>
      <h1>我的影片應用</h1>
      <app-player-view
        [data]="videoData"
        (onPaused)="handlePaused()"
      />
    </div>
  `,
})
export class AppComponent {
  videoData = signal<z.infer<typeof myCompSchema>>({
    titleText: '你好,Angular + Remotion!',
    titleColor: '#ffffff',
    logoColor1: '#91EAE4',
    logoColor2: '#86A8E7',
  });
 
  handlePaused() {
    console.log('播放器已暫停');
  }
 
  // 更新影片屬性
  updateTitle(newTitle: string) {
    this.videoData.set({
      ...this.videoData(),
      titleText: newTitle,
    });
  }
}

啟動 Remotion Studio

在你的 Angular 專案根目錄中執行:

npx remotion studio

此命令會啟動 Remotion Studio,讓你可以預覽和調試你的影片組件。

另請參閱