{"version":3,"file":"av-recorder.umd.cjs","sources":["../src/av-recorder.ts"],"sourcesContent":["import {\n  Log,\n  recodemux,\n  autoReadStream,\n  EventTool,\n  file2stream,\n} from '@webav/internal-utils';\nimport {\n  AVRecorderConf,\n  IStream,\n  IRecordeOpts as IRecordOpts,\n  TClearFn,\n} from './types';\n\ntype TState = 'inactive' | 'recording' | 'paused' | 'stopped';\n\n/**\n * 录制媒体流 MediaStream，生成 MP4 文件流\n *\n * 如果你期望录制为 WebM 格式，请使用 [MediaRecorder](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder)\n *\n * @example\n * const recorder = new AVRecorder(\n * await navigator.mediaDevices.getUserMedia({\n *   video: true,\n *   audio: true,\n * })\n);\n\nrecorder.start() // => ReadableStream\n * @see [录制摄像头](https://webav-tech.github.io/WebAV/demo/4_1-recorder-usermedia)\n */\nexport class AVRecorder {\n  #state: TState = 'inactive';\n  get state(): TState {\n    return this.#state;\n  }\n  set state(_: TState) {\n    throw new Error('state is readonly');\n  }\n\n  #evtTool = new EventTool<{\n    stateChange: (state: TState) => void;\n  }>();\n  on = this.#evtTool.on;\n\n  #conf: Omit<IRecordOpts, 'timeSlice'>;\n\n  #recoderPauseCtrl: RecoderPauseCtrl;\n\n  constructor(inputMediaStream: MediaStream, conf: AVRecorderConf = {}) {\n    this.#conf = createRecoderConf(inputMediaStream, conf);\n    this.#recoderPauseCtrl = new RecoderPauseCtrl(this.#conf.video.expectFPS);\n  }\n\n  #stopStream = () => {};\n  /**\n   * 开始录制，返回 MP4 文件流\n   * @param timeSlice 控制流输出数据的时间间隔，单位毫秒\n   *\n   */\n  start(timeSlice: number = 500): ReadableStream<Uint8Array> {\n    if (this.#state === 'stopped') throw Error('AVRecorder is stopped');\n    Log.info('AVRecorder.start recoding');\n\n    const { streams } = this.#conf;\n\n    if (streams.audio == null && streams.video == null) {\n      throw new Error('No available tracks in MediaStream');\n    }\n\n    const { stream, exit } = startRecord(\n      { timeSlice, ...this.#conf },\n      this.#recoderPauseCtrl,\n      () => {\n        this.stop();\n      },\n    );\n    this.#stopStream();\n    this.#stopStream = exit;\n    return stream;\n  }\n\n  /**\n   * 暂停录制\n   */\n  pause(): void {\n    this.#state = 'paused';\n    this.#recoderPauseCtrl.pause();\n    this.#evtTool.emit('stateChange', this.#state);\n  }\n  /**\n   * 恢复录制\n   */\n  resume(): void {\n    if (this.#state === 'stopped') throw Error('AVRecorder is stopped');\n    this.#state = 'recording';\n    this.#recoderPauseCtrl.play();\n    this.#evtTool.emit('stateChange', this.#state);\n  }\n\n  /**\n   * 停止\n   */\n  async stop(): Promise<void> {\n    if (this.#state === 'stopped') return;\n    this.#state = 'stopped';\n\n    this.#stopStream();\n  }\n}\n\nfunction createRecoderConf(inputMS: MediaStream, userConf: AVRecorderConf) {\n  const conf = {\n    bitrate: 3e6,\n    expectFPS: 30,\n    videoCodec: 'avc1.42E032',\n    ...userConf,\n  };\n  const { streams, width, height, sampleRate, channelCount } =\n    extractMSSettings(inputMS);\n\n  const opts: Omit<IRecordOpts, 'timeSlice'> = {\n    video: {\n      width: width ?? 1280,\n      height: height ?? 720,\n      expectFPS: conf.expectFPS,\n      codec: conf.videoCodec,\n    },\n    audio: {\n      codec: 'aac',\n      sampleRate: sampleRate ?? 44100,\n      channelCount: channelCount ?? 2,\n    },\n    bitrate: conf.bitrate,\n    streams,\n  };\n  return opts;\n}\n\nfunction extractMSSettings(inputMS: MediaStream) {\n  const videoTrack = inputMS.getVideoTracks()[0];\n  const settings: MediaTrackSettings & { streams: IStream } = { streams: {} };\n  if (videoTrack != null) {\n    Object.assign(settings, videoTrack.getSettings());\n    settings.streams.video = new MediaStreamTrackProcessor({\n      track: videoTrack,\n    }).readable;\n  }\n\n  const audioTrack = inputMS.getAudioTracks()[0];\n  if (audioTrack != null) {\n    Object.assign(settings, audioTrack.getSettings());\n    Log.info('AVRecorder recording audioConf:', settings);\n    settings.streams.audio = new MediaStreamTrackProcessor({\n      track: audioTrack,\n    }).readable;\n  }\n\n  return settings;\n}\n\nclass RecoderPauseCtrl {\n  // 当前帧的偏移时间，用于计算帧的 timestamp\n  #offsetTime = performance.now();\n\n  // 编码上一帧的时间，用于计算出当前帧的持续时长\n  #lastTime = this.#offsetTime;\n\n  // 用于限制 帧率\n  #frameCnt = 0;\n\n  // 如果为true，则暂停编码数据\n  // 取消暂停时，需要减去\n  #paused = false;\n\n  // 触发暂停的时间，用于计算暂停持续了多久\n  #pauseTime = 0;\n\n  // 间隔多少帧生成一个关键帧\n  #gopSize = 30;\n\n  constructor(readonly expectFPS: number) {\n    this.#gopSize = Math.floor(expectFPS * 3);\n  }\n\n  start() {\n    this.#offsetTime = performance.now();\n    this.#lastTime = this.#offsetTime;\n  }\n\n  play() {\n    if (!this.#paused) return;\n    this.#paused = false;\n\n    this.#offsetTime += performance.now() - this.#pauseTime;\n    this.#lastTime += performance.now() - this.#pauseTime;\n  }\n\n  pause() {\n    if (this.#paused) return;\n    this.#paused = true;\n    this.#pauseTime = performance.now();\n  }\n\n  transfromVideo(frame: VideoFrame) {\n    const now = performance.now();\n    const offsetTime = now - this.#offsetTime;\n    if (\n      this.#paused ||\n      // 避免帧率超出期望太高\n      (this.#frameCnt / offsetTime) * 1000 > this.expectFPS\n    ) {\n      frame.close();\n      return;\n    }\n\n    const vf = new VideoFrame(frame, {\n      // timestamp 单位 微秒\n      timestamp: offsetTime * 1000,\n      duration: (now - this.#lastTime) * 1000,\n    });\n    this.#lastTime = now;\n\n    this.#frameCnt += 1;\n    frame.close();\n    return {\n      vf,\n      opts: { keyFrame: this.#frameCnt % this.#gopSize === 0 },\n    };\n  }\n\n  transformAudio(ad: AudioData) {\n    if (this.#paused) {\n      ad.close();\n      return;\n    }\n    return ad;\n  }\n}\n\nfunction startRecord(\n  opts: IRecordOpts,\n  ctrl: RecoderPauseCtrl,\n  onEnded: TClearFn,\n) {\n  let stopEncodeVideo: TClearFn | null = null;\n  let stopEncodeAudio: TClearFn | null = null;\n\n  const [hasVideoTrack, hasAudioTrack] = [\n    opts.streams.video != null,\n    opts.streams.audio != null && opts.audio != null,\n  ];\n\n  const recoder = recodemux({\n    video: hasVideoTrack\n      ? { ...opts.video, bitrate: opts.bitrate ?? 3_000_000 }\n      : null,\n    audio: hasAudioTrack ? opts.audio : null,\n  });\n\n  let stoped = false;\n  if (hasVideoTrack) {\n    let lastVf: VideoFrame | null = null;\n    let autoInsertVFTimer = 0;\n    const emitVf = (vf: VideoFrame) => {\n      clearTimeout(autoInsertVFTimer);\n\n      lastVf?.close();\n      lastVf = vf;\n      const vfWrap = ctrl.transfromVideo(vf.clone());\n      if (vfWrap == null) return;\n      recoder.encodeVideo(vfWrap.vf, vfWrap.opts);\n\n      // 录制静态画面，MediaStream 不出帧时，每秒插入一帧\n      autoInsertVFTimer = self.setTimeout(() => {\n        if (lastVf == null) return;\n        const newVf = new VideoFrame(lastVf, {\n          timestamp: lastVf.timestamp + 1e6,\n          duration: 1e6,\n        });\n        emitVf(newVf);\n      }, 1000);\n    };\n\n    ctrl.start();\n    const stopReadStream = autoReadStream(opts.streams.video!, {\n      onChunk: async (chunk: VideoFrame) => {\n        if (stoped) {\n          chunk.close();\n          return;\n        }\n        emitVf(chunk);\n      },\n      onDone: () => {},\n    });\n\n    stopEncodeVideo = () => {\n      stopReadStream();\n      clearTimeout(autoInsertVFTimer);\n      lastVf?.close();\n    };\n  }\n\n  if (hasAudioTrack) {\n    stopEncodeAudio = autoReadStream(opts.streams.audio!, {\n      onChunk: async (ad: AudioData) => {\n        if (stoped) {\n          ad.close();\n          return;\n        }\n        const newAD = ctrl.transformAudio(ad);\n        if (newAD != null) recoder.encodeAudio(ad);\n      },\n      onDone: () => {},\n    });\n  }\n\n  const { stream, stop: stopStream } = file2stream(\n    recoder.mp4file,\n    opts.timeSlice,\n    () => {\n      exit();\n      onEnded();\n    },\n  );\n\n  function exit() {\n    stoped = true;\n\n    stopEncodeVideo?.();\n    stopEncodeAudio?.();\n    recoder.close();\n    stopStream();\n  }\n\n  return { exit, stream };\n}\n"],"names":["AVRecorder","#state","_","#evtTool","EventTool","#conf","#recoderPauseCtrl","inputMediaStream","conf","createRecoderConf","RecoderPauseCtrl","#stopStream","timeSlice","Log","streams","stream","exit","startRecord","inputMS","userConf","width","height","sampleRate","channelCount","extractMSSettings","videoTrack","settings","audioTrack","expectFPS","#gopSize","#offsetTime","#lastTime","#frameCnt","#paused","#pauseTime","frame","now","offsetTime","vf","ad","opts","ctrl","onEnded","stopEncodeVideo","stopEncodeAudio","hasVideoTrack","hasAudioTrack","recoder","recodemux","stoped","lastVf","autoInsertVFTimer","emitVf","vfWrap","newVf","stopReadStream","autoReadStream","chunk","stopStream","file2stream"],"mappings":"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"}