[ TypeScript ] MediaReaderでMediaStreamを保存
60383 ワード
イントロ
MediaReaderで動画やオーディオを保存してみます.
私はwebrtcを試みるために作成したプロジェクトを使用します.
ベースプロジェクト
インデックス.京大理
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello WebRTC</title>
<meta charset="utf-8">
</head>
<body>
...
<div id="webrtc_sample_area">
...
<video id="local_video" muted>Video stream not available.</video>
<video id="received_video" autoplay>Video stream not available.</video>
</div>
<div>
<button onclick="Page.switchFrame()">Frame</button>
<button onclick="Page.startRecording()">Start</button>
<button onclick="Page.stopRecording()">Stop</button>
</div>
<canvas id="picture_canvas"></canvas>
<a id="download_target"></a>
<script src="js/main.js"></script>
</body>
</html>
メイン.ページ。TS
import { VideoRecorder } from "./video-recorder";
import { WebRtcController } from "./webrtc-controller";
...
let rtcSample = new WebRtcController();
let videoRecorder: VideoRecorder;
...
export function startRecording(): void {
videoRecorder.startRecording();
}
export function stopRecording(): void {
videoRecorder.stopRecording();
}
export function switchFrame(): void {
videoRecorder.updateCanvasSize();
videoRecorder.switchFrame();
}
...
function init(){
rtcSample = new WebRtcController();
rtcSample.initVideo();
videoRecorder = new VideoRecorder();
}
init();
WEBTCコントローラTS
...
export class WebRtcController {
...
public initVideo(){
const localVideo = document.getElementById("local_video") as HTMLVideoElement;
let streaming = false;
// after being UserMedia available, set Video element's size.
localVideo.addEventListener("canplay", () => {
if (streaming === false) {
const width = 320;
const height = localVideo.videoHeight / (localVideo.videoWidth/width);
localVideo.setAttribute("width", width.toString());
localVideo.setAttribute("height", height.toString());
streaming = true;
}
}, false);
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
this.webcamStream = stream;
localVideo.srcObject = stream;
localVideo.play();
streaming = true;
})
.catch(err => console.error(`An error occurred: ${err}`));
}
...
保存ビデオとオーディオ
私はビデオやオーディオを保存することができます
ビデオレコーダー。TS
export class VideoRecorder {
private recorder: MediaRecorder|null = null;
public startRecording() {
const localVideo = document.getElementById("local_video") as HTMLVideoElement;
const localVideoStream = this.getVideoStream(this.localVideo);
if(localVideoStream != null) {
this.recorder = new MediaRecorder(localVideoStream, this.getMimeType());
this.recorder.ondataavailable = (ev) => this.saveRecordedVideo(ev);
this.recorder.start();
}
}
public stopRecording() {
this.recorder?.stop();
}
private getVideoStream(targetElement: HTMLVideoElement): MediaStream|null {
if(targetElement.srcObject != null &&
"getTracks" in targetElement.srcObject &&
typeof targetElement.srcObject.getTracks === "function" &&
"addTrack" in targetElement.srcObject &&
typeof targetElement.srcObject.addTrack === "function") {
return targetElement.srcObject;
}
return null;
}
private saveRecordedVideo(ev: BlobEvent): void {
if(ev.data.size <= 0) {
return;
}
const url = URL.createObjectURL(ev.data);
const downloadTarget = document.getElementById("download_target") as HTMLAnchorElement;
downloadTarget.download = "sample.webm";
downloadTarget.href = url;
downloadTarget.click();
}
private getMimeType(): MediaRecorderOptions {
if(MediaRecorder.isTypeSupported("video/webm; codecs=vp9")) {
return { mimeType: "video/webm; codecs=vp9" };
}
if(MediaRecorder.isTypeSupported("video/webm; codecs=vp8")) {
return { mimeType: "video/webm; codecs=vp8" };
}
return { mimeType: "video/webm" };
}
}
コーデック
デフォルトでは、私はWebMとしてビデオを保存することができます.
FirefoxがVP 9を扱うことができないので、私はMIMEタイプのVP 8を加えます.
mediastreamを合成する
例えば、私はビデオでオーディオ、オーディオを加えたいです.
オーディオ
私は1つのメディアストリームに複数のビデオトラックやオーディオトラックを追加することはできません.
それは最初のトラックを扱うだけです.
...
public startRecording() {
const localVideo = document.getElementById("local_video") as HTMLVideoElement;
const localVideoStream = this.getVideoStream(this.localVideo);
if(localVideoStream != null) {
// these two rows are ignored,
// because localVideoStream already has a video track and an audio track.
localVideoStream.addTrack(someVideoStream);
localVideoStream.addTrack(someAudioStream);
this.recorder = new MediaRecorder(localVideoStream, this.getMimeType());
this.recorder.ondataavailable = (ev) => this.saveRecordedVideo(ev);
this.recorder.start();
}
}
...
だから私は複数のオーディオをマージするWebオーディオAPIを使用します.ビデオレコーダー。TS
export class VideoRecorder {
private recorder: MediaRecorder|null = null;
private localVideo: HTMLVideoElement;
private recording = false;
private mixedAudioDestinationNode: MediaStreamAudioDestinationNode|null = null;
public constructor() {
this.localVideo = document.getElementById("local_video") as HTMLVideoElement;
this.localVideo.onplay = () => this.init();
}
public startRecording() {
this.recording = true;
const videoTrack = this.getVideoStream(this.localVideo)?.getVideoTracks()[0]!;
if(videoTrack == null) {
return;
}
const newStream = new MediaStream();
if(this.mixedAudioDestinationNode != null) {
newStream.addTrack(this.mixedAudioDestinationNode.stream.getAudioTracks()[0]!);
}
newStream.addTrack(videoTrack);
this.recorder = new MediaRecorder(newStream, this.getMimeType());
this.recorder.start();
this.recorder.ondataavailable = (ev) => this.saveRecordedVideo(ev);
}
public stopRecording() {
this.recording = false;
this.recorder?.stop();
}
...
private init(): void {
const localVideoStream = this.getVideoStream(this.localVideo);
if(localVideoStream != null) {
this.createMixedAudio(localVideoStream);
}
}
...
private createMixedAudio(stream: MediaStream): void {
const audioContext = new AudioContext();
const audioSourceNode = audioContext.createMediaStreamSource(stream);
const delay = new DelayNode(audioContext);
delay.delayTime.value = 1;
const splitter = audioContext.createChannelSplitter(2);
audioSourceNode.connect(splitter);
splitter.connect(delay, 1);
const merger = audioContext.createChannelMerger(2);
delay.connect(merger, 0, 1);
splitter.connect(merger, 1, 0);
this.mixedAudioDestinationNode = audioContext.createMediaStreamDestination();
merger.connect(audioContext.destination);
merger.connect(this.mixedAudioDestinationNode);
}
...
}
絵
私はキャンバスの要素からMediaStreamを作成することができます.
そして、1つのmediastreamに複数のビデオトラックを加えることができないので.
それで、私はキャンバス要素でイメージとしてビデオを描きます.
それに画像を追加した後、私はそれからMediaStreamを作成します.
ビデオレコーダー。TS
export class VideoRecorder {
private recorder: MediaRecorder|null = null;
private localVideo: HTMLVideoElement;
private recording = false;
private pictureCanvas: HTMLCanvasElement;
private frameShown = false;
private frameImage: HTMLImageElement|null = null;
private mixedAudioDestinationNode: MediaStreamAudioDestinationNode|null = null;
public constructor() {
this.localVideo = document.getElementById("local_video") as HTMLVideoElement;
this.localVideo.onplay = () => this.init();
this.pictureCanvas = document.getElementById("picture_canvas") as HTMLCanvasElement;
this.pictureCanvas.style.position = "absolute";
}
public startRecording() {
this.recording = true;
const pictureStream = this.pictureCanvas.captureStream(60);
const pictureTrack = pictureStream.getVideoTracks()[0];
if(pictureTrack == null) {
console.error("No picture video tracks");
return;
}
const newStream = new MediaStream();
if(this.mixedAudioDestinationNode != null) {
newStream.addTrack(this.mixedAudioDestinationNode.stream.getAudioTracks()[0]!);
}
newStream.addTrack(pictureTrack);
this.recorder = new MediaRecorder(newStream, this.getMimeType());
this.recorder.start();
this.updatePictureCanvas(this.pictureCanvas.getContext("2d") as CanvasRenderingContext2D);
this.recorder.ondataavailable = (ev) => this.saveRecordedVideo(ev);
}
public stopRecording() {
this.recording = false;
this.recorder?.stop();
}
public updateCanvasSize(): void {
this.pictureCanvas.width = this.localVideo.videoWidth;
this.pictureCanvas.height = this.localVideo.videoHeight;
const rect = this.localVideo.getBoundingClientRect();
this.pictureCanvas.style.top = `${rect.top}px`;
this.pictureCanvas.style.left = `${rect.left}px`;
const ctx = this.pictureCanvas.getContext("2d") as CanvasRenderingContext2D;
this.frameImage = new Image();
this.frameImage.onload = () => this.drawFrameImage(ctx);
this.frameImage.src = "../img/frame.png";
}
public switchFrame(): void {
if(this.frameShown === true) {
this.pictureCanvas.style.display = "none";
this.frameShown = false;
} else {
this.pictureCanvas.style.display = "block";
this.frameShown = true;
}
}
private init(): void {
const localVideoStream = this.getVideoStream(this.localVideo);
if(localVideoStream != null) {
this.createMixedAudio(localVideoStream);
}
}
private getVideoStream(targetElement: HTMLVideoElement): MediaStream|null {
if(targetElement.srcObject != null &&
"getTracks" in targetElement.srcObject &&
typeof targetElement.srcObject.getTracks === "function" &&
"addTrack" in targetElement.srcObject &&
typeof targetElement.srcObject.addTrack === "function") {
return targetElement.srcObject;
}
return null;
}
private createMixedAudio(stream: MediaStream): void {
const audioContext = new AudioContext();
const audioSourceNode = audioContext.createMediaStreamSource(stream);
const delay = new DelayNode(audioContext);
delay.delayTime.value = 1;
const splitter = audioContext.createChannelSplitter(2);
audioSourceNode.connect(splitter);
splitter.connect(delay, 1);
const merger = audioContext.createChannelMerger(2);
delay.connect(merger, 0, 1);
splitter.connect(merger, 1, 0);
this.mixedAudioDestinationNode = audioContext.createMediaStreamDestination();
merger.connect(audioContext.destination);
merger.connect(this.mixedAudioDestinationNode);
}
private saveRecordedVideo(ev: BlobEvent): void {
if(ev.data.size <= 0) {
console.error("No video data");
return;
}
const url = URL.createObjectURL(ev.data);
const downloadTarget = document.getElementById("download_target") as HTMLAnchorElement;
downloadTarget.download = "sample.webm";
downloadTarget.href = url;
downloadTarget.click();
}
private getMimeType(): MediaRecorderOptions {
if(MediaRecorder.isTypeSupported("video/webm; codecs=vp9")) {
return { mimeType: "video/webm; codecs=vp9" };
}
if(MediaRecorder.isTypeSupported("video/webm; codecs=vp8")) {
return { mimeType: "video/webm; codecs=vp8" };
}
return { mimeType: "video/webm" };
}
private updatePictureCanvas(ctx: CanvasRenderingContext2D) {
if(this.recording === false) {
return;
}
this.drawFrameImage(ctx);
// To save as video, I have to redraw the images.
setTimeout(() => this.updatePictureCanvas(ctx), 1000.0 / 60.0);
}
private drawFrameImage(ctx: CanvasRenderingContext2D): void {
if(this.frameImage == null) {
return;
}
ctx.drawImage(this.localVideo, 0, 0, this.localVideo.videoWidth, this.localVideo.videoHeight);
ctx.drawImage(this.frameImage, 0, 0, this.localVideo.videoWidth, this.localVideo.videoHeight);
}
}
資源
Reference
この問題について([ TypeScript ] MediaReaderでMediaStreamを保存), 我々は、より多くの情報をここで見つけました https://dev.to/masanori_msl/typescript-save-mediastream-by-mediarecorder-13hhテキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol