録画

  • /*
     * @title 録画
     * @description 画面をwebmで録画するやつ
     * @include http://*
     * @include https://*
     * @license MIT License
     * @javascript_url
     */
    
    (function() {
        'use strict';
      
        if (!navigator.mediaDevices) {
          alert(location.protocol === 'http:' ? 'httpsでないサイトでは使えません。' : '対応していないブラウザです');
          return;
        }
    
        const tag = (name, props = {}, children = []) => {
            const e = Object.assign(document.createElement(name), props);
            if (typeof props.style === "object") Object.assign(e.style, props.style);
            (children.forEach ? children : [children]).forEach(c => e.appendChild(c));
            return e;
        };
    
        const modal = (() => {
            let refs = {};
            refs.container = tag('div', {
                style: 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 2147483647'
            }, [
                refs.content = tag('div', {
                    style: `
    position: absolute; width: 320px; height: 160px; left: 0; right: 0; top: 0; bottom: 0; margin: auto; padding: 1em;
    background: white; border-radius: 5px;
    display: flex; flex-direction: column; align-items: center; justify-content: space-evenly;`
                }, [
                    tag('p', { style: 'font-size: 1.4em; font-weight: bold', textContent: '録画中...' }),
                    tag('p', { textContent: '下の「録画終了」ボタンをクリックするか、画面共有を停止すると、録画を終了します。' }),
                    refs.stopButton = tag('button', { textContent: '録画終了' })
                ])
            ]);
            return {
                show({onStop}) {
                    document.body.appendChild(refs.container);
                    refs.stopButton.onclick = onStop;
                },
                hide() {
                    refs.container.remove();
                }
            };
        })();
    
        function saveFile(filename, blob) {
            const url = URL.createObjectURL(blob);
            const link = tag('a', { style: 'display: none', href: url, download: filename });
            document.body.appendChild(link);
            link.click();
            URL.revokeObjectURL(url);
            link.remove();
        }
    
        function getDefaultFilename() {
            const dateStr = new Date().toLocaleString('ja-JP', {
                hour12: false,
                year: 'numeric',
                month: '2-digit',
                day: '2-digit',
                hour: '2-digit',
                minute: '2-digit',
                second: '2-digit'
            });
          return dateStr.replace(/[\/:]/g, '').replace(/ /, '-');
        }
    
        navigator.mediaDevices.getDisplayMedia(/* w,h,audio */)
        .then(stream => {
            let chunks = [];
            const recorder = new MediaRecorder(stream, {
                mimeType: MediaRecorder.isTypeSupported('video/webm;codecs=vp9') ? 'video/webm;codecs=vp9' : 'video/webm;codecs=vp8',
                videoBitsPerSecond: 800 * 1000
            });
            recorder.ondataavailable = e => chunks.push(e.data);
            const recordingPromise = new Promise((resolve, reject) => {
                recorder.onstop = () => resolve(new Blob(chunks, { type: recorder.mimeType }));
                recorder.onerror = e => reject(e.error);
            })
            .finally(() => {
                // 録画を終了したら元ストリームも閉じる(共有を停止する)
                stream.getTracks().forEach(track => track.stop());
                modal.hide();
            });
            // 82canaryだとなくても共有終了で閉じてくれたけど、80stableでは必要っぽい?
            stream.getTracks().forEach(x => x.addEventListener('ended', () => recorder.stop()));
            modal.show({ onStop: () => recorder.stop() });
            recorder.start();
            return recordingPromise;
        })
        .then(blob => {
            const name = prompt('録画が完了しました。ファイル名を入力してください。', getDefaultFilename());
            if (!name) return;
            saveFile(name + '.webm', blob);
        })
        .catch(e => {
          const hint =
            e.message.includes('user gesture') ? '一度ページのどこかをクリックしてから再実行してみてください。' :
            e.name === 'NotAllowedError' ? 'キャプチャがブロックされているようです。\n・共有ダイアログで「拒否」を選択しませんでしたか?\n・ブラウザの設定でカメラの使用などをブロックしていませんか?' :
            null;
          const msg = e.message || e.name || e;
          alert('録画に失敗しました。\n' + (hint ? hint + '\n\n' : '') + 'エラー詳細: ' + e);
        });
    })();
  • Permalink
    このページへの個別リンクです。
    RAW
    書かれたコードへの直接のリンクです。
    Packed
    文字列が圧縮された書かれたコードへのリンクです。
    Userscript
    Greasemonkey 等で利用する場合の .user.js へのリンクです。
    Loader
    @require やソースコードが長い場合に多段ロードする Loader コミのコードへのリンクです。
    Metadata
    コード中にコメントで @xxx と書かれたメタデータの JSON です。

History

  1. 2020/02/12 15:52:31 - 2020-02-12
  2. 2020/02/12 15:21:52 - 2020-02-12
  3. 2020/02/12 15:06:53 - 2020-02-12
  4. 2020/02/12 14:57:25 - 2020-02-12
  5. 2020/02/12 14:55:13 - 2020-02-12
  6. 2020/02/12 14:54:26 - 2020-02-12
  7. 2020/02/12 14:51:46 - 2020-02-12
  8. 2020/02/12 14:51:37 - 2020-02-12
  9. 2020/02/12 14:49:21 - 2020-02-12
  10. 2020/02/12 14:48:09 - 2020-02-12