ブラウザで動画再生・加工・録画

ブラウザで動画再生・加工・録画

javascriptで動画加工をする

ブラウザで画像処理に続いて動画像処理。

  • 動画ファイルを選択
  • 選択した動画を再生
  • 加工した動画を再生
  • 加工した動画をダウンロード

これらをブラウザ上で実現。
HTML5とjavascript、優秀。

1. デモ

  1. 動画ファイル(MP4)を選択すると読み込まれる
  2. 動画を再生すると並行して加工(グレイスケール化)した動画も再生される
  3. 再生が終了すると加工動画のダウンロード用リンクが作られる
  4. ダウンロード用リンク先からダウンロード(多分最後まで再生しないとダウンロードできない)

再生位置を変えて終了させた場合も画面に映った通りの動画がダウンロードできる。
スマホは無理。
バグ対応などはしていない。

2. 実装

特別にライブラリなど使わずに実装できる。
個々の機能説明の後、全コードを載せる。

2.1. mp4ファイル読みこみ・再生

input type="file"のタグを使ってファイル選択UIを作成。
ファイル選択後にURL.createObjectURLで選択したファイルを取得してvideoに渡す。

これによってユーザの選択したローカルのmp4動画ファイルを読みこみ、ブラウザで再生できる。

HTML

1
2
3
4
<input type="file" id="selectedFile" name="file" />
<video id="inputVideo" controls="true" width="480" height="270" crossorigin="anonymous">
<source type="video/mp4">
</video>

JS

1
2
3
4
5
6
7
8
9
10
11
// HTML要素取得
let selectedFile = document.getElementById("selectedFile");
let inputVideo = document.getElementById("inputVideo");

// 動画再生イベント追加
selectedFile.addEventListener("change", loadVideo, false);

// 関数1 「ファイル選択」 ファイルが選択されたら実行、選択された動画を読み込む
async function loadVideo() {
inputVideo.src = URL.createObjectURL(selectedFile.files[0]);
}

2.2. javascriptで動画加工

addEventListenerで動画読み込み後に実行するイベントを追加する。
画像と違って動画の場合は"load"ではなく"loadedmetadata"とする。

実際にフレームを加工する部分はmozillaのサンプルを参考にした。

  1. drawImageを使ってvideoで再生中の画面をcanvasに貼り付け
  2. getImageDataでcanvas内の画像データを1次元配列として取得
  3. 配列に対して画像処理を行いputImageDataで再度canvasに描画

今回はグレイスケール化の処理だが、コード内の関数5 「画像処理」の中身を変えると別の加工が行える。

https://developer.mozilla.org/ja/docs/Web/Guide/Audio_and_video_manipulation

HTML

1
2
3
4
<video id="inputVideo" controls="true" width="480" height="270" crossorigin="anonymous">
<source type="video/mp4">
</video>
<canvas id="output" width="480" height="270"></canvas>

JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// HTML要素取得
let output = document.getElementById('output');

// 動画加工用イベント追加
inputVideo.addEventListener("loadedmetadata", processVideo, false);


// 関数2 「ファイル読み込み」 選択された動画が読み込まれたら実行、動画加工
async function processVideo() {
videoProcessor.videoInitialize();
}



// オブジェクト 「動画加工」 動画再生と並行して動画加工してcanvasに描く
const videoProcessor = {
// 関数3 「初期化」 動画処理の準備
videoInitialize: function() {
this.video = document.getElementById("inputVideo");
this.c1 = document.getElementById("output");
this.ctx1 = this.c1.getContext("2d");
let self = this;
//videoが再生されたら加工を始めるためのイベントを追加
this.video.addEventListener("play", function() {
self.width = self.video.width;
self.height = self.video.height;
requestAnimationFrame(self.timerCallback.bind(self));
}, false);
},

// 関数4 「フレーム更新」 枚フレーム処理を行う
timerCallback: function() {
if (this.video.paused || this.video.ended) {return;}
this.computeFrame();
let self = this;
requestAnimationFrame(self.timerCallback.bind(self));
return;
},

// 関数5 「画像処理」 フレームを取得して画像処理を行う
computeFrame: function() {
this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
let frame = this.ctx1.getImageData(0, 0, this.width, this.height);

for (let i = 0; i < (frame.data.length / 4); i++) { // frameにはRGBAが1次元で並んでいるので4ずつ回す
let grey = (frame.data[i * 4 + 0] + frame.data[i * 4 + 1] + frame.data[i * 4 + 2]) / 3; //RGB平均
frame.data[i * 4 + 0] = grey;
frame.data[i * 4 + 1] = grey;
frame.data[i * 4 + 2] = grey;
}
this.ctx1.putImageData(frame, 0, 0);
return;
}
};

2.3. MediaRecorderでcanvasを録画

加工した動画を録画することで加工動画を取得する。

canvasの録画はMediaRecorderという関数で簡単にできる。
mp4で保存したかったが対応していないようなのでwebmで妥協。

  1. canvas要素に対してcaptureStream()でストリーム取得
  2. MediaRecorder()に取得したストリームを渡してrecorderを作成
  3. recorder.start()で録画開始
  4. recorder.stop()で録画終了

videoにaddEventListenerでイベント追加することで録画タイミングを操作。
動画再生と同時に録画開始、
動画の終了時に録画終了するようにした。

HTML

1
<canvas id="output" width="480" height="270"></canvas>

JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 動画録画用要素・ストリーム取得
let stream = document.getElementById('output').captureStream();
let recorder = new MediaRecorder(stream, {mimeType:'video/webm;codecs=vp9'});

// オブジェクト 「動画加工」 動画再生と並行して動画加工してcanvasに描く
const videoProcessor = {
// 関数4 「初期化」 動画処理の準備
videoInitialize: function() {
this.video = document.getElementById("inputVideo");
this.c1 = document.getElementById("output");
this.ctx1 = this.c1.getContext("2d");
let self = this;
//videoが再生されたら加工を始めるためのイベントを追加
this.video.addEventListener("play", function() {
self.width = self.video.width;
self.height = self.video.height;
if(recorder.state != "recording"){recorder.start();} // 録画開始
requestAnimationFrame(self.timerCallback.bind(self));
}, false);

//videoが停止・終了したら録画を終わらせるためのイベントを追加(pauseだと停止 endedだと終了)
this.video.addEventListener("ended", function() {//pause or ended
recorder.stop() // 録画終了
}, false);
},
};

2.4. 録画した動画をダウンロード

録画した加工動画をダウンロードする。

recorderにイベントを追加、録画終了時は"dataavailable"イベントが使える。
動画の一時URLを取得してaタグに書き込んだものをHTMLに挿入したら完成。

JS

1
2
3
4
5
6
7
8
9
10
// 加工した動画の保存用イベント追加
recorder.addEventListener("dataavailable", (e) => {makeLink(e)}, false)

// 関数3 「リンク作成」 録画した動画をダウンロードするためのリンク作成
async function makeLink(e) {
const videoBlob = new Blob([e.data], { type: e.data.type });
const blobUrl = window.URL.createObjectURL(videoBlob);
const linktext = '<p><a href="' + blobUrl + ' " target="_blank">ダウンロード</a></p>'
output.insertAdjacentHTML('afterend', linktext);
}

3. コード

今回のデモで使用したコード。

HTML

1
2
3
4
5
6
7
<input type="file" id="selectedFile" name="file" />

<video id="inputVideo" controls="true" width="480" height="270" crossorigin="anonymous">
<source type="video/mp4">
</video>

<canvas id="output" width="480" height="270"></canvas>

JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// 動画再生・加工用HTML要素取得
let selectedFile = document.getElementById("selectedFile");
let inputVideo = document.getElementById("inputVideo");
let output = document.getElementById('output');
// 動画再生・加工用イベント追加
selectedFile.addEventListener("change", loadVideo, false);
inputVideo.addEventListener("loadedmetadata", processVideo, false);

// 動画録画用要素・ストリーム取得
let stream = document.getElementById('output').captureStream();
let recorder = new MediaRecorder(stream, {mimeType:'video/webm;codecs=vp9'});
// 加工した動画の保存用イベント追加
recorder.addEventListener("dataavailable", (e) => {makeLink(e)}, false)


// 関数1 「ファイル選択」 ファイルが選択されたら実行、選択された動画を読み込む
async function loadVideo() {
inputVideo.src = URL.createObjectURL(selectedFile.files[0]);
}

// 関数2 「ファイル読み込み」 選択された動画が読み込まれたら実行、動画加工
async function processVideo() {
videoProcessor.videoInitialize();
}

// 関数3 「リンク作成」 録画した動画をダウンロードするためのリンク作成
async function makeLink(e) {
const videoBlob = new Blob([e.data], { type: e.data.type });
const blobUrl = window.URL.createObjectURL(videoBlob);
const linktext = '<p><a href="' + blobUrl + ' " target="_blank">ダウンロード</a></p>'
// const linktext = '<p><a href="' + blobUrl + ' " download="">ダウンロード</a></p>'
output.insertAdjacentHTML('afterend', linktext);
}

// オブジェクト 「動画加工」 動画再生と並行して動画加工してcanvasに描く
const videoProcessor = {
// 関数4 「初期化」 動画処理の準備
videoInitialize: function() {
this.video = document.getElementById("inputVideo");
this.c1 = document.getElementById("output");
this.ctx1 = this.c1.getContext("2d");
let self = this;
//videoが再生されたら加工を始めるためのイベントを追加
this.video.addEventListener("play", function() {
self.width = self.video.width;
self.height = self.video.height;
if(recorder.state != "recording"){recorder.start();} // 録画開始
requestAnimationFrame(self.timerCallback.bind(self));
}, false);
//videoが停止・終了したら録画を終わらせるためのイベントを追加(pauseだと停止 endedだと終了)
this.video.addEventListener("ended", function() {//pause or ended
recorder.stop() // 録画終了
}, false);
},

// 関数5 「フレーム更新」 枚フレーム処理を行う
timerCallback: function() {
if (this.video.paused || this.video.ended) {return;}
this.computeFrame();
let self = this;
requestAnimationFrame(self.timerCallback.bind(self));
return;
},

// 関数6 「画像処理」 フレームを取得して画像処理を行う
computeFrame: function() {
this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
let frame = this.ctx1.getImageData(0, 0, this.width, this.height);

for (let i = 0; i < (frame.data.length / 4); i++) { // frameにはRGBAが1次元で並んでいるので4ずつ回す
let grey = (frame.data[i * 4 + 0] + frame.data[i * 4 + 1] + frame.data[i * 4 + 2]) / 3; //RGB平均
frame.data[i * 4 + 0] = grey;
frame.data[i * 4 + 1] = grey;
frame.data[i * 4 + 2] = grey;
}
this.ctx1.putImageData(frame, 0, 0);
return;
}
};

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×