황현동 블로그 개발, 인생, 유우머

251221 MP4Box와 WebCodec API 완벽 가이드

Tags:

MP4Box와 WebCodec API 완벽 가이드

웹 브라우저에서의 고성능 비디오 처리가 새로운 전환점을 맞고 있습니다. MP4Box.js와 WebCodec API의 결합은 네이티브 애플리케이션 수준의 비디오 조작 능력을 웹에서 구현할 수 있게 해줍니다.

기술 개요

MP4Box.js란?

MP4Box.js는 GPAC 프로젝트의 JavaScript 포트로, MP4 컨테이너 형식을 완전히 파싱하고 조작할 수 있는 강력한 라이브러리입니다.

┌────────────── MP4Box.js 핵심 기능 ──────────────┐
│                                               │
│  ┌─────────────┐    ┌─────────────────────┐   │
│  │   MP4 File  │───►│   MP4Box Parser     │   │
│  │   Stream    │    │                     │   │
│  └─────────────┘    │ • Track Extraction  │   │
│                     │ • Metadata Reading  │   │
│  ┌─────────────┐    │ • Sample Access     │   │
│  │ Segmented   │◄───│ • Live Streaming    │   │
│  │   Output    │    │ • Format Conversion │   │
│  └─────────────┘    └─────────────────────┘   │
│                                               │
│  지원 기능:                                    │
│  • DASH/HLS 세그멘테이션                       │
│  • 실시간 스트림 처리                          │
│  • 메타데이터 편집                             │
│  • 다중 트랙 관리                              │
└───────────────────────────────────────────────┘

WebCodec API 소개

WebCodec API는 브라우저에서 네이티브 수준의 비디오/오디오 인코딩과 디코딩을 가능하게 하는 새로운 웹 표준입니다.

// WebCodec API 기본 구조
const decoder = new VideoDecoder({
  output: (frame) => {
    // 디코딩된 프레임 처리
    console.log('Frame decoded:', frame);
  },
  error: (error) => {
    console.error('Decode error:', error);
  }
});

// 인코더 설정
const encoder = new VideoEncoder({
  output: (chunk, metadata) => {
    // 인코딩된 청크 처리
    console.log('Chunk encoded:', chunk);
  },
  error: (error) => {
    console.error('Encode error:', error);
  }
});

MP4Box.js 심화 활용

1. MP4 파일 파싱과 메타데이터 추출

import MP4Box from 'mp4box';

class MP4Analyzer {
  constructor() {
    this.mp4boxfile = MP4Box.createFile();
    this.setupEventHandlers();
  }
  
  setupEventHandlers() {
    // 파일이 준비되면 호출
    this.mp4boxfile.onReady = (info) => {
      console.log('MP4 정보:', info);
      console.log('총 재생 시간:', info.duration / info.timescale, '');
      console.log('트랙 수:', info.tracks.length);
      
      // 각 트랙 정보 출력
      info.tracks.forEach((track, index) => {
        console.log(`트랙 ${index + 1}:`, {
          id: track.id,
          type: track.type,
          codec: track.codec,
          width: track.video?.width,
          height: track.video?.height,
          sampleRate: track.audio?.sample_rate,
          channels: track.audio?.channel_count
        });
      });
    };
    
    // 에러 처리
    this.mp4boxfile.onError = (error) => {
      console.error('MP4 파싱 에러:', error);
    };
    
    // 세그먼트 추출 완료
    this.mp4boxfile.onSegment = (id, user, buffer) => {
      console.log('세그먼트 추출됨:', { id, buffer: buffer.byteLength });
    };
  }
  
  async loadFile(file) {
    const arrayBuffer = await file.arrayBuffer();
    arrayBuffer.fileStart = 0;
    
    // MP4Box에 데이터 공급
    this.mp4boxfile.appendBuffer(arrayBuffer);
  }
  
  extractVideoTrack() {
    const info = this.mp4boxfile.getInfo();
    const videoTrack = info.tracks.find(track => track.type === 'video');
    
    if (videoTrack) {
      // 비디오 트랙의 모든 샘플 추출
      this.mp4boxfile.setExtractionOptions(videoTrack.id, null, {
        nbSamples: 100 // 처음 100개 샘플만
      });
      
      this.mp4boxfile.start();
    }
  }
  
  createDASHSegments() {
    const info = this.mp4boxfile.getInfo();
    
    info.tracks.forEach(track => {
      // DASH 세그멘테이션 설정
      this.mp4boxfile.setSegmentOptions(track.id, null, {
        duration: 2000, // 2초 세그먼트
        rapAlignement: true
      });
    });
    
    this.mp4boxfile.start();
  }
}

// 사용 예시
const analyzer = new MP4Analyzer();

// 파일 선택 이벤트
document.getElementById('fileInput').addEventListener('change', async (event) => {
  const file = event.target.files[0];
  if (file) {
    await analyzer.loadFile(file);
    analyzer.extractVideoTrack();
  }
});

2. 실시간 스트림 처리

class MP4StreamProcessor {
  constructor() {
    this.mp4boxfile = MP4Box.createFile();
    this.setupStreamHandling();
  }
  
  setupStreamHandling() {
    this.mp4boxfile.onReady = (info) => {
      console.log('스트림 준비 완료');
      
      // 각 트랙에 대해 실시간 추출 설정
      info.tracks.forEach(track => {
        this.mp4boxfile.setExtractionOptions(track.id, this, {
          nbSamples: 1000
        });
      });
      
      this.mp4boxfile.start();
    };
    
    // 샘플이 추출될 때마다 호출
    this.mp4boxfile.onSamples = (id, user, samples) => {
      console.log(`트랙 ${id}에서 ${samples.length}개 샘플 추출`);
      
      samples.forEach(sample => {
        this.processSample(sample);
      });
    };
  }
  
  processSample(sample) {
    // 샘플 데이터를 WebCodec으로 전달
    const chunk = new EncodedVideoChunk({
      type: sample.is_sync ? 'key' : 'delta',
      timestamp: sample.cts,
      duration: sample.duration,
      data: sample.data
    });
    
    // 디코더로 전달
    this.decoder?.decode(chunk);
  }
  
  async processStream(stream) {
    const reader = stream.getReader();
    let offset = 0;
    
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      
      // ArrayBuffer에 오프셋 정보 추가
      value.fileStart = offset;
      offset += value.byteLength;
      
      // MP4Box에 청크 공급
      this.mp4boxfile.appendBuffer(value);
    }
  }
}

// 네트워크 스트림 처리 예시
const processor = new MP4StreamProcessor();

fetch('https://example.com/video.mp4')
  .then(response => processor.processStream(response.body))
  .catch(console.error);

WebCodec API 고급 활용

1. 하드웨어 가속 비디오 디코딩

class HardwareAcceleratedDecoder {
  constructor(config) {
    this.frames = [];
    this.decoder = null;
    this.init(config);
  }
  
  async init(config) {
    // 하드웨어 지원 확인
    const support = await VideoDecoder.isConfigSupported(config);
    console.log('디코더 지원:', support);
    
    if (support.supported) {
      this.decoder = new VideoDecoder({
        output: (frame) => this.handleFrame(frame),
        error: (error) => this.handleError(error)
      });
      
      this.decoder.configure(config);
    }
  }
  
  handleFrame(frame) {
    console.log('프레임 디코딩 완료:', {
      timestamp: frame.timestamp,
      duration: frame.duration,
      codedWidth: frame.codedWidth,
      codedHeight: frame.codedHeight
    });
    
    // Canvas에 프레임 그리기
    this.drawFrameToCanvas(frame);
    
    // 메모리 해제
    frame.close();
  }
  
  drawFrameToCanvas(frame) {
    const canvas = document.getElementById('videoCanvas');
    const ctx = canvas.getContext('2d');
    
    // 캔버스 크기 조정
    canvas.width = frame.codedWidth;
    canvas.height = frame.codedHeight;
    
    // VideoFrame을 ImageBitmap으로 변환하여 그리기
    createImageBitmap(frame).then(bitmap => {
      ctx.drawImage(bitmap, 0, 0);
      bitmap.close();
    });
  }
  
  handleError(error) {
    console.error('디코딩 에러:', error);
  }
  
  decode(chunk) {
    if (this.decoder?.state === 'configured') {
      this.decoder.decode(chunk);
    }
  }
  
  close() {
    this.decoder?.close();
  }
}

// 사용 예시
const decoderConfig = {
  codec: 'avc1.42E01E', // H.264 Baseline Profile
  codedWidth: 1920,
  codedHeight: 1080,
  hardwareAcceleration: 'prefer-hardware'
};

const decoder = new HardwareAcceleratedDecoder(decoderConfig);

2. 실시간 비디오 인코딩

class RealTimeVideoEncoder {
  constructor(config) {
    this.encoder = null;
    this.chunks = [];
    this.init(config);
  }
  
  async init(config) {
    const support = await VideoEncoder.isConfigSupported(config);
    
    if (support.supported) {
      this.encoder = new VideoEncoder({
        output: (chunk, metadata) => this.handleEncodedChunk(chunk, metadata),
        error: (error) => this.handleError(error)
      });
      
      this.encoder.configure(config);
    }
  }
  
  handleEncodedChunk(chunk, metadata) {
    console.log('청크 인코딩 완료:', {
      type: chunk.type,
      timestamp: chunk.timestamp,
      byteLength: chunk.byteLength
    });
    
    // 청크를 배열에 저장
    const chunkData = new Uint8Array(chunk.byteLength);
    chunk.copyTo(chunkData);
    
    this.chunks.push({
      data: chunkData,
      type: chunk.type,
      timestamp: chunk.timestamp,
      duration: chunk.duration,
      metadata: metadata
    });
    
    // 실시간 스트리밍이나 저장 처리
    this.processEncodedChunk(chunk, metadata);
  }
  
  processEncodedChunk(chunk, metadata) {
    // WebRTC를 통한 실시간 전송이나
    // 파일 저장 등의 처리를 여기서 수행
  }
  
  handleError(error) {
    console.error('인코딩 에러:', error);
  }
  
  async encodeFrame(videoFrame) {
    if (this.encoder?.state === 'configured') {
      // 키프레임 요청 여부 결정
      const keyFrame = this.chunks.length % 30 === 0; // 30프레임마다 키프레임
      
      this.encoder.encode(videoFrame, { keyFrame });
    }
  }
  
  async flush() {
    await this.encoder?.flush();
  }
  
  close() {
    this.encoder?.close();
  }
  
  exportVideo() {
    // 인코딩된 청크들을 MP4 파일로 결합
    return this.createMP4FromChunks();
  }
  
  createMP4FromChunks() {
    const mp4boxfile = MP4Box.createFile();
    
    // MP4Box를 사용해 청크들을 MP4 컨테이너에 패키징
    // 구현 세부사항은 용도에 따라 달라짐
  }
}

// 웹캠에서 실시간 인코딩 예시
const encoderConfig = {
  codec: 'avc1.42E01E',
  width: 1280,
  height: 720,
  bitrate: 2000000, // 2Mbps
  framerate: 30,
  hardwareAcceleration: 'prefer-hardware'
};

const encoder = new RealTimeVideoEncoder(encoderConfig);

// 웹캠 스트림 캡처
navigator.mediaDevices.getUserMedia({ video: true })
  .then(stream => {
    const video = document.createElement('video');
    video.srcObject = stream;
    video.play();
    
    video.addEventListener('loadedmetadata', () => {
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      
      function captureFrame() {
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;
        ctx.drawImage(video, 0, 0);
        
        // Canvas에서 VideoFrame 생성
        const frame = new VideoFrame(canvas, {
          timestamp: performance.now() * 1000
        });
        
        encoder.encodeFrame(frame);
        frame.close();
        
        requestAnimationFrame(captureFrame);
      }
      
      captureFrame();
    });
  });

MP4Box와 WebCodec 통합 활용

1. 고성능 비디오 편집기

class WebVideoEditor {
  constructor() {
    this.mp4box = MP4Box.createFile();
    this.decoder = null;
    this.encoder = null;
    this.timeline = [];
    this.setupMP4Box();
  }
  
  setupMP4Box() {
    this.mp4box.onReady = (info) => {
      console.log('비디오 로드 완료:', info);
      this.setupCodecs(info);
    };
    
    this.mp4box.onSamples = (id, user, samples) => {
      samples.forEach(sample => this.processSample(sample));
    };
  }
  
  async setupCodecs(info) {
    const videoTrack = info.tracks.find(track => track.type === 'video');
    
    if (videoTrack) {
      // 디코더 설정
      this.decoder = new VideoDecoder({
        output: (frame) => this.processFrame(frame),
        error: (error) => console.error('Decode error:', error)
      });
      
      this.decoder.configure({
        codec: videoTrack.codec,
        codedWidth: videoTrack.video.width,
        codedHeight: videoTrack.video.height
      });
      
      // 인코더 설정 (편집된 비디오 출력용)
      this.encoder = new VideoEncoder({
        output: (chunk, metadata) => this.handleEditedChunk(chunk, metadata),
        error: (error) => console.error('Encode error:', error)
      });
      
      this.encoder.configure({
        codec: 'avc1.42E01E',
        width: videoTrack.video.width,
        height: videoTrack.video.height,
        bitrate: 5000000,
        framerate: 30
      });
    }
  }
  
  processSample(sample) {
    const chunk = new EncodedVideoChunk({
      type: sample.is_sync ? 'key' : 'delta',
      timestamp: sample.cts,
      duration: sample.duration,
      data: sample.data
    });
    
    this.decoder.decode(chunk);
  }
  
  processFrame(frame) {
    // 프레임에 효과 적용
    const editedFrame = this.applyEffects(frame);
    
    // 편집된 프레임 인코딩
    this.encoder.encode(editedFrame);
    
    // 메모리 정리
    frame.close();
    if (editedFrame !== frame) {
      editedFrame.close();
    }
  }
  
  applyEffects(frame) {
    // Canvas를 사용한 실시간 효과 적용
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    
    canvas.width = frame.codedWidth;
    canvas.height = frame.codedHeight;
    
    // 프레임을 캔버스에 그리기
    createImageBitmap(frame).then(bitmap => {
      ctx.drawImage(bitmap, 0, 0);
      
      // 효과 적용 (예: 필터, 색상 보정 등)
      this.applyFilter(ctx, canvas.width, canvas.height);
      
      bitmap.close();
    });
    
    // 편집된 캔버스에서 새 VideoFrame 생성
    return new VideoFrame(canvas, {
      timestamp: frame.timestamp
    });
  }
  
  applyFilter(ctx, width, height) {
    const imageData = ctx.getImageData(0, 0, width, height);
    const data = imageData.data;
    
    // 간단한 세피아 필터
    for (let i = 0; i < data.length; i += 4) {
      const r = data[i];
      const g = data[i + 1];
      const b = data[i + 2];
      
      data[i] = Math.min(255, (r * 0.393) + (g * 0.769) + (b * 0.189));
      data[i + 1] = Math.min(255, (r * 0.349) + (g * 0.686) + (b * 0.168));
      data[i + 2] = Math.min(255, (r * 0.272) + (g * 0.534) + (b * 0.131));
    }
    
    ctx.putImageData(imageData, 0, 0);
  }
  
  handleEditedChunk(chunk, metadata) {
    console.log('편집된 청크:', chunk.byteLength, 'bytes');
    // 편집된 비디오 청크를 저장하거나 스트리밍
  }
  
  async loadVideo(file) {
    const arrayBuffer = await file.arrayBuffer();
    arrayBuffer.fileStart = 0;
    this.mp4box.appendBuffer(arrayBuffer);
  }
  
  startEditing() {
    const info = this.mp4box.getInfo();
    const videoTrack = info.tracks.find(track => track.type === 'video');
    
    if (videoTrack) {
      this.mp4box.setExtractionOptions(videoTrack.id, this, {
        nbSamples: 1000
      });
      this.mp4box.start();
    }
  }
}

// 사용 예시
const editor = new WebVideoEditor();

document.getElementById('videoInput').addEventListener('change', async (event) => {
  const file = event.target.files[0];
  if (file) {
    await editor.loadVideo(file);
    editor.startEditing();
  }
});

2. 적응형 스트리밍 플레이어

class AdaptiveStreamPlayer {
  constructor(videoElement) {
    this.videoElement = videoElement;
    this.mp4box = MP4Box.createFile();
    this.decoder = null;
    this.qualities = [];
    this.currentQuality = 0;
    this.setupPlayer();
  }
  
  setupPlayer() {
    this.mp4box.onReady = (info) => {
      this.initializeDecoder(info);
    };
    
    this.mp4box.onSamples = (id, user, samples) => {
      this.decodeSamples(samples);
    };
  }
  
  async initializeDecoder(info) {
    const videoTrack = info.tracks.find(track => track.type === 'video');
    
    this.decoder = new VideoDecoder({
      output: (frame) => this.displayFrame(frame),
      error: (error) => console.error('Decoder error:', error)
    });
    
    this.decoder.configure({
      codec: videoTrack.codec,
      codedWidth: videoTrack.video.width,
      codedHeight: videoTrack.video.height,
      hardwareAcceleration: 'prefer-hardware'
    });
  }
  
  decodeSamples(samples) {
    samples.forEach(sample => {
      const chunk = new EncodedVideoChunk({
        type: sample.is_sync ? 'key' : 'delta',
        timestamp: sample.cts,
        duration: sample.duration,
        data: sample.data
      });
      
      this.decoder.decode(chunk);
    });
  }
  
  displayFrame(frame) {
    // VideoFrame을 video 엘리먼트에 표시
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    
    canvas.width = frame.codedWidth;
    canvas.height = frame.codedHeight;
    
    createImageBitmap(frame).then(bitmap => {
      ctx.drawImage(bitmap, 0, 0);
      
      // Canvas의 stream을 video 엘리먼트에 연결
      if (!this.videoElement.srcObject) {
        this.videoElement.srcObject = canvas.captureStream();
      }
      
      bitmap.close();
    });
    
    frame.close();
  }
  
  async loadSegment(segmentUrl) {
    try {
      const response = await fetch(segmentUrl);
      const arrayBuffer = await response.arrayBuffer();
      arrayBuffer.fileStart = 0;
      
      this.mp4box.appendBuffer(arrayBuffer);
    } catch (error) {
      console.error('세그먼트 로딩 에러:', error);
    }
  }
  
  // 네트워크 상태에 따른 품질 조정
  adjustQuality() {
    const connection = navigator.connection;
    
    if (connection) {
      const effectiveType = connection.effectiveType;
      
      switch (effectiveType) {
        case '4g':
          this.currentQuality = 2; // 고화질
          break;
        case '3g':
          this.currentQuality = 1; // 중화질
          break;
        default:
          this.currentQuality = 0; // 저화질
          break;
      }
      
      console.log('품질 조정:', this.qualities[this.currentQuality]);
    }
  }
  
  startPlayback() {
    const videoTrack = this.mp4box.getInfo().tracks.find(track => track.type === 'video');
    
    if (videoTrack) {
      this.mp4box.setExtractionOptions(videoTrack.id, this, {
        nbSamples: 100
      });
      this.mp4box.start();
    }
  }
}

// 사용 예시
const player = new AdaptiveStreamPlayer(document.getElementById('adaptiveVideo'));

// HLS 매니페스트 파싱 및 세그먼트 로딩 (의사 코드)
async function playHLSStream(manifestUrl) {
  const manifest = await parseHLSManifest(manifestUrl);
  player.qualities = manifest.qualities;
  
  // 첫 세그먼트 로딩
  await player.loadSegment(manifest.segments[0].url);
  player.startPlayback();
  
  // 나머지 세그먼트들을 순차적으로 로딩
  for (let i = 1; i < manifest.segments.length; i++) {
    await player.loadSegment(manifest.segments[i].url);
  }
}

성능 최적화 및 브라우저 지원

브라우저 지원 현황

// 기능 지원 감지
class FeatureDetection {
  static checkWebCodecSupport() {
    const results = {
      videoDecoder: typeof VideoDecoder !== 'undefined',
      videoEncoder: typeof VideoEncoder !== 'undefined',
      audioDecoder: typeof AudioDecoder !== 'undefined',
      audioEncoder: typeof AudioEncoder !== 'undefined'
    };
    
    console.log('WebCodec 지원:', results);
    return results;
  }
  
  static async checkCodecSupport() {
    const codecs = [
      'avc1.42E01E', // H.264 Baseline
      'avc1.4D401E', // H.264 Main
      'avc1.64001E', // H.264 High
      'hev1.1.6.L93.B0', // H.265
      'av01.0.04M.08', // AV1
      'vp8', // VP8
      'vp09.00.10.08' // VP9
    ];
    
    for (const codec of codecs) {
      try {
        const decoderSupport = await VideoDecoder.isConfigSupported({
          codec,
          codedWidth: 1920,
          codedHeight: 1080
        });
        
        const encoderSupport = await VideoEncoder.isConfigSupported({
          codec,
          width: 1920,
          height: 1080,
          bitrate: 5000000,
          framerate: 30
        });
        
        console.log(`${codec}:`, {
          decoder: decoderSupport.supported,
          encoder: encoderSupport.supported
        });
        
      } catch (error) {
        console.log(`${codec}: 지원되지 않음`);
      }
    }
  }
  
  static checkMP4BoxSupport() {
    try {
      const mp4box = MP4Box.createFile();
      console.log('MP4Box 지원: 예');
      return true;
    } catch (error) {
      console.log('MP4Box 지원: 아니요');
      return false;
    }
  }
}

// 사용 예시
FeatureDetection.checkWebCodecSupport();
FeatureDetection.checkCodecSupport();
FeatureDetection.checkMP4BoxSupport();

메모리 관리 최적화

class MemoryOptimizedProcessor {
  constructor() {
    this.framePool = [];
    this.maxPoolSize = 10;
    this.activeFrames = new Set();
  }
  
  getFrame() {
    if (this.framePool.length > 0) {
      return this.framePool.pop();
    }
    return null; // 새 프레임 생성 필요
  }
  
  releaseFrame(frame) {
    if (this.framePool.length < this.maxPoolSize) {
      this.framePool.push(frame);
    } else {
      frame.close();
    }
    
    this.activeFrames.delete(frame);
  }
  
  trackFrame(frame) {
    this.activeFrames.add(frame);
    
    // 메모리 사용량 모니터링
    if (this.activeFrames.size > 20) {
      console.warn('너무 많은 프레임이 활성 상태입니다:', this.activeFrames.size);
    }
  }
  
  cleanupAll() {
    this.activeFrames.forEach(frame => frame.close());
    this.activeFrames.clear();
    
    this.framePool.forEach(frame => frame.close());
    this.framePool = [];
  }
  
  // 메모리 사용량 리포팅
  getMemoryStats() {
    return {
      activeFrames: this.activeFrames.size,
      pooledFrames: this.framePool.length,
      estimatedMemoryMB: (this.activeFrames.size + this.framePool.length) * 8 // 대략적인 추정
    };
  }
}

// 가비지 컬렉션 최적화
class GCOptimizedDecoder {
  constructor(config) {
    this.decoder = new VideoDecoder({
      output: (frame) => this.handleFrame(frame),
      error: (error) => console.error(error)
    });
    
    this.decoder.configure(config);
    this.processedFrames = 0;
    this.gcThreshold = 100;
  }
  
  handleFrame(frame) {
    // 프레임 처리
    this.processFrame(frame);
    
    // 프레임 해제
    frame.close();
    
    this.processedFrames++;
    
    // 주기적으로 가비지 컬렉션 유도
    if (this.processedFrames % this.gcThreshold === 0) {
      // 작은 지연을 두고 GC 실행 유도
      setTimeout(() => {
        if (window.gc) {
          window.gc(); // Chrome DevTools에서만 사용 가능
        }
      }, 0);
    }
  }
  
  processFrame(frame) {
    // 실제 프레임 처리 로직
  }
}

실무 활용 사례

1. 브라우저 기반 비디오 편집 도구

class BrowserVideoEditor {
  constructor() {
    this.tracks = [];
    this.effects = [];
    this.timeline = new VideoTimeline();
  }
  
  async addVideoTrack(file) {
    const track = new VideoTrack(file);
    await track.initialize();
    this.tracks.push(track);
    return track;
  }
  
  addEffect(trackIndex, effect, startTime, duration) {
    this.effects.push({
      trackIndex,
      effect,
      startTime,
      duration
    });
  }
  
  async renderVideo(outputConfig) {
    const renderer = new VideoRenderer(outputConfig);
    
    for (let time = 0; time < this.timeline.duration; time += 1/30) {
      const frame = await this.renderFrameAtTime(time);
      renderer.addFrame(frame);
    }
    
    return await renderer.finalize();
  }
  
  async renderFrameAtTime(time) {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    
    // 각 트랙에서 해당 시간의 프레임 가져오기
    for (const track of this.tracks) {
      const frame = await track.getFrameAtTime(time);
      if (frame) {
        ctx.drawImage(frame, 0, 0);
        frame.close();
      }
    }
    
    // 효과 적용
    this.applyEffectsAtTime(ctx, time);
    
    return new VideoFrame(canvas, { timestamp: time * 1000000 });
  }
  
  applyEffectsAtTime(ctx, time) {
    for (const effect of this.effects) {
      if (time >= effect.startTime && time <= effect.startTime + effect.duration) {
        effect.effect.apply(ctx, time - effect.startTime);
      }
    }
  }
}

2. 실시간 화상회의 시스템

class WebRTCVideoProcessor {
  constructor() {
    this.localStream = null;
    this.encoder = null;
    this.decoder = null;
    this.peerConnections = new Map();
  }
  
  async initializeLocalVideo() {
    this.localStream = await navigator.mediaDevices.getUserMedia({
      video: { width: 1280, height: 720 }
    });
    
    // 인코더 설정
    this.encoder = new VideoEncoder({
      output: (chunk) => this.broadcastChunk(chunk),
      error: (error) => console.error('Encode error:', error)
    });
    
    this.encoder.configure({
      codec: 'avc1.42E01E',
      width: 1280,
      height: 720,
      bitrate: 1000000,
      framerate: 30,
      latencyMode: 'realtime'
    });
    
    this.startCapturing();
  }
  
  startCapturing() {
    const video = document.createElement('video');
    video.srcObject = this.localStream;
    video.play();
    
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    
    const captureFrame = () => {
      if (video.readyState === video.HAVE_ENOUGH_DATA) {
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;
        ctx.drawImage(video, 0, 0);
        
        const frame = new VideoFrame(canvas, {
          timestamp: performance.now() * 1000
        });
        
        this.encoder.encode(frame, { keyFrame: false });
        frame.close();
      }
      
      requestAnimationFrame(captureFrame);
    };
    
    video.addEventListener('loadedmetadata', captureFrame);
  }
  
  broadcastChunk(chunk) {
    // 모든 피어에게 인코딩된 청크 전송
    for (const [peerId, connection] of this.peerConnections) {
      this.sendChunkToPeer(peerId, chunk);
    }
  }
  
  sendChunkToPeer(peerId, chunk) {
    const data = new Uint8Array(chunk.byteLength);
    chunk.copyTo(data);
    
    const message = {
      type: 'video-chunk',
      data: data,
      timestamp: chunk.timestamp,
      chunkType: chunk.type
    };
    
    // WebRTC 데이터 채널을 통해 전송
    const connection = this.peerConnections.get(peerId);
    if (connection && connection.dataChannel) {
      connection.dataChannel.send(JSON.stringify(message));
    }
  }
  
  handleRemoteChunk(chunk) {
    if (!this.decoder) {
      this.initializeDecoder();
    }
    
    const encodedChunk = new EncodedVideoChunk({
      type: chunk.chunkType,
      timestamp: chunk.timestamp,
      data: chunk.data
    });
    
    this.decoder.decode(encodedChunk);
  }
  
  initializeDecoder() {
    this.decoder = new VideoDecoder({
      output: (frame) => this.displayRemoteFrame(frame),
      error: (error) => console.error('Decode error:', error)
    });
    
    this.decoder.configure({
      codec: 'avc1.42E01E',
      codedWidth: 1280,
      codedHeight: 720
    });
  }
  
  displayRemoteFrame(frame) {
    const remoteVideo = document.getElementById('remoteVideo');
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    
    canvas.width = frame.codedWidth;
    canvas.height = frame.codedHeight;
    
    createImageBitmap(frame).then(bitmap => {
      ctx.drawImage(bitmap, 0, 0);
      
      if (!remoteVideo.srcObject) {
        remoteVideo.srcObject = canvas.captureStream();
      }
      
      bitmap.close();
    });
    
    frame.close();
  }
}

미래 전망과 발전 방향

웹 플랫폼의 미디어 처리 기술은 계속해서 발전하고 있습니다:

1. 하드웨어 가속의 확산

  • GPU 기반 비디오 처리의 표준화
  • 전용 미디어 프로세서 지원 확대
  • 모바일 디바이스에서의 성능 최적화

2. 새로운 코덱 지원

  • AV1 코덱의 광범위한 지원
  • VVC(H.266) 표준화 진행
  • 실시간 스트리밍 최적화된 코덱들

3. WebAssembly 통합

  • 고성능 미디어 라이브러리의 웹 포팅
  • FFmpeg 웹 버전의 성능 향상
  • 커스텀 코덱 구현의 용이성

결론

MP4Box.js와 WebCodec API의 조합은 웹에서 네이티브급 비디오 처리를 가능하게 만들었습니다. 이러한 기술들을 통해:

  • 전문적인 비디오 편집: 브라우저에서 완전한 비디오 편집 도구 구현
  • 실시간 스트리밍: 지연 시간 없는 고품질 라이브 스트리밍
  • 효율적인 압축: 하드웨어 가속을 활용한 실시간 인코딩/디코딩
  • 적응형 재생: 네트워크 상황에 맞는 동적 품질 조정

웹 플랫폼이 미디어 처리의 새로운 표준으로 자리잡고 있는 현재, 이러한 기술들을 마스터하는 것은 미래 웹 애플리케이션 개발에 있어 필수적인 역량이 될 것입니다.