Vue项目中录音


由于本人的项目主要是 Vue3 + electron 进行开发,本文也以Vue3进行讲解:

1.使用 electron + navigator.mediaDevices.getUserMedia 录制音视频

想获取视频流,首先需要获取所需要捕获视频流的 MediaSourceIdElectron 提供了一个获取各个“窗口”和“屏幕”视频 MediaSourceId 的通用 API

import { desktopCapturer } from 'electron';

// 获取全部窗口或屏幕的mediaSourceId
desktopCapturer.getSources({
    types: ['screen', 'window'], // 设定需要捕获的是"屏幕",还是"窗口"
    thumbnailSize: {
        height: 300, // 窗口或屏幕的截图快照高度
        width: 300 // 窗口或屏幕的截图快照宽度
    },
    fetchWindowIcons: true // 如果视频源是窗口且有图标,则设置该值可以捕获到的窗口图标
}).then(sources => {
    sources.forEach(source => {
        // 如果视频源是窗口且有图标,且fetchWindowIcons设为true,则为捕获到的窗口图标
        console.log(source.appIcon);

        // 显示器Id
        console.log(source.display_id);

        // 视频源的mediaSourceId,可通过该mediaSourceId获取视频源
        console.log(source.id);

        // 窗口名,通常来说与任务管理器看到的进程名一致
        console.log(source.name);

        // 窗口或屏幕在调用本API瞬间抓捕到的截图快照
        console.log(source.thumbnail);
    });
});

如果你只想获取当前窗口的 MediaSourceID

import { remote } from 'electron';
// const { remote } = require("electron");

// 获取当前窗口mediaSourceId的做法
const mediaSourceId = remote.getCurrentWindow().getMediaSourceId();

Windows音频流获取

const mediaRecorder = ref(null);
const audioRef=ref(null);//html写 <audio src="" id="download" ref="audioRef" controls></audio>

let recordedChunks = [];
const setSource = async () => {
    try {
        const stream = await navigator.mediaDevices.getUserMedia({
            // audio: true, // 强行表示不录制音频,音频额外获取,但是录制的是麦克风的声音
            // video: false,

            /* 上面简单写法只能录制麦克风的声音,若是需要录制系统声音,需要使用以下方法 */
            audio: {
                mandatory: {
                    // 无需指定mediaSourceId就可以录音了,录得是系统音频
                    chromeMediaSource: "desktop",
                },
            },
            video: {
                // 如果想要录制音频,必须同样把视频的选项带上,否则会失败
                mandatory: {
                    chromeMediaSource: "desktop",
                    chromeMediaSourceId: mediaSourceId,
                },
            },
        });

        // 若是不需要视频源,手工移除点不用的视频源,即可完成音频流的获取
        (stream.getVideoTracks() || []).forEach((track) =>
            stream.removeTrack(track)
        );
        // 去除音频源 getAudioTracks() 不过一般录制视频的时候,基本上都会保留音频源
        // (stream.getAudioTracks() || []).forEach((track) =>
        //     stream.removeTrack(track)
        // );

        var options = {
            audioBitsPerSecond: 128000, //音频比特率为 128kbps
            videoBitsPerSecond: 2500000, //视频比特率为 2.5Mbps
            mimeType:"audio/webm; codecs=pcm"
        };
        console.log(stream, "stream");
        mediaRecorder.value = new MediaRecorder(stream,options);// options 可不携带

        // 更新流
        mediaRecorder.value.addEventListener("dataavailable", function (e) {
            if (e.data.size > 0) {
                recordedChunks.push(e.data);
            }
        });

        mediaRecorder.value.addEventListener("pause", (e) => {
            console.log("暂停");
        });

        mediaRecorder.value.addEventListener("resume", (e) => {
            console.log("继续");
        });

        mediaRecorder.value.addEventListener("stop", function () {
            console.log(
                "停止",
                recordedChunks,
                new Blob(recordedChunks,{ type: "audio/webm; codecs=pcm" }),
                URL.createObjectURL(new Blob(recordedChunks,{ type: "audio/webm; codecs=pcm" }))
            );

            // 写入 audio里
            audioRef.value.src = URL.createObjectURL(new Blob(recordedChunks));
        });

        mediaRecorder.value.addEventListener("start", (e) => {
            console.log("开始");
        });
    } catch (e) {
        console.log(e);
    }
};

onMounted(() => {
    setSource();
})
// 操作按钮
// 开始
const startButton = () => {
    recordedChunks=[];
    mediaRecorder.value.start(1000);//1s读取一次
};

// 停止
const stopButton = () => {
    mediaRecorder.value.stop();
};

// 暂停/继续
const pauseButton = () => {
    console.log(mediaRecorder.value.state);
    if (mediaRecorder.value.state === "recording") {
        mediaRecorder.value.pause();
    } else if (mediaRecorder.value.state === "paused") {
        mediaRecorder.value.resume();
    }
};

2.使用 Recorder + navigator.mediaDevices.getUserMedia 录音音视频

Recorder链接:Recorder

import Recorder from "recorder-core";
import "recorder-core/src/engine/wav";
import "recorder-core/src/extensions/wavesurfer.view";

const fs = window.require("fs");
const { remote } = require("electron");
const { dialog } = remote;

const mediaRecorder = ref(null);
let rec = null;

const indexStr = ref("");
const index = ref(0);

const wave = ref();//绘制波形图
const blobData = ref();

let timer = null;

const recStart=(stream) => {
    console.log("未开始录音", stream);
    if (rec) {
        //清理掉已有的
        rec.close();
    }

    rec = Recorder({
        type: "wav",
        sampleRate: 16000,
        bitRate: 16,
        sourceStream: stream, //明确指定从这个流中录制音频
        onProcess: function (
            buffers,
            powerLevel,
            bufferDuration,
            bufferSampleRate
        ) {
            wave.value = Recorder.WaveSurferView({ elem: ".elem" });
            wave.value.input(
                buffers[buffers.length - 1],
                powerLevel,
                bufferSampleRate
            );
        },
    });
    rec.open(
        function () {
            setTimeout(() => {
                rec.start();
            }, 1000);
            timer = setInterval(() => {
                index.value += 1;
                getTime(index.value);
            }, 1000);
        },
        function (msg, isUserNotAllow) {
            console.log((isUserNotAllow ? "UserNotAllow," : "") + "无法录音:" + msg);
        }
    );
}

const setSource = async () => {
    try {
        const stream = await navigator.mediaDevices.getUserMedia({
            // audio: {
            //     deviceId: deviceId
            // },
            //上面的可以通过 deviceId 选择麦克风设备进行录音
            // 下面的是系统录音
            audio: {
                mandatory: {
                    // 无需指定mediaSourceId就可以录音了,录得是系统音频
                    chromeMediaSource: "desktop",
                },
            },
            video: {
                // 如果想要录制音频,必须同样把视频的选项带上,否则会失败
                mandatory: {
                    chromeMediaSource: "desktop",
                    chromeMediaSourceId: mediaSourceId,
                },
            },
        });

        // 接着手工移除点不用的视频源,即可完成音频流的获取
        (stream.getVideoTracks() || []).forEach((track) =>
            stream.removeTrack(track)
        );
        recStart(stream);
        console.log(stream, "stream");
    } catch (e) {
        console.log(e);
    }
};

const startButton = () => {
    indexStr.value = "00:00:00";
    setSource();
};

const stopButton = () => {
    clearInterval(timer);
    timer = null;
    rec.stop(
            function (blob, duration) {
                if (blob) {
                    blobData.value = blob;
                }
                console.log(blob,'blob');
            },
            function (msg) {
                console.log("录音失败:" + msg);
            }
        );
};

const getTime = (index) => {
    if (index < 10) {
        indexStr.value = "00:00:0" + index;
    }
    if (index > 10 && index < 60) {
        indexStr.value = "00:00:" + index;
    }
    if (index > 60 && index < 3600) {
        let miao = index % 60 < 10 ? "0" + (index % 60) : index % 60;
        let fen =
            parseInt(index / 60) < 10
                ? "0" + parseInt(index / 60)
                : parseInt(index / 60);
        indexStr.value = "00:" + fen + ":" + miao;
    }
};
// 写入 导出音频
const exportAudio = () => {
    dialog
        .showSaveDialog(null, {
            title: "保存",
            defaultPath: "recorder" + ".wav",
            properties: ["openFile"],
            filters: [{ name: "All Files", extensions: ["*"] }],
            // 点击保存回调
        })
        .then(({ filePath }) => {
            const fr = new FileReader();
            fr.readAsArrayBuffer(blobData.value);
            setTimeout(() => {
                let buffer = fr.result;
                var buf = new Buffer(buffer.byteLength);
                var view = new Uint8Array(buffer);
                for (var i = 0; i < buf.length; ++i) {
                    buf[i] = view[i];
                }
                fs.writeFile(filePath, buf, (err) => {
                    console.log(err, "err");
                });
            });
        });
};

在 方法1 中,最后导出音频的时候,在 mediaRecorder.value.addEventListener("stop", function () {})里 添加 blobData.value=new Blob(recordedChunks,{ type: "audio/wav" }),并调用 exportAudio方法,导致的音频并不是 wav 格式

原因是recordedChunks流里的格式是 audio/webm; codecs=pcm,在 new Blob里时候,并没有改变 recordedChunksmimeType的格式,仅仅是给Blob定义了一个type

并且mimeType里并没有 audio/wav这个格式配置 ,所以不仅在 options 里配置无效,还会报错。

tips:目前的 chrome 浏览器的采样率是只读状态且不能修改(修改无效), 需要在每一次onaudioprocess 触发的时候对 PCM 数据进行相应的转化处理,来达到使用要求。 推荐一个转化采样率的方法: Recorder源码
——来源:浏览器的录音

目前官方支持的格式:

const types = [
  "video/webm",
  "audio/webm",
  "video/webm;codecs=vp8",
  "video/webm;codecs=daala",
  "video/webm;codecs=h264",
  "audio/webm;codecs=opus",
  "video/mpeg"
];

使用 isTypeSupported 测试当前浏览器的支持状况

for (const type of types) {
    console.log(`Is ${type} supported? ${MediaRecorder.isTypeSupported(type) ? "Maybe!" : "Nope :("}`);
}

3. 扩展

1.使用 enumerateDevices()检查可用的输入设备

let itemOpiton=[]
navigator.mediaDevices.enumerateDevices().then((devices) => {
    devices.forEach((device) => {
        console.log(device,'device');
        if (device.kind === "audioinput") {
            itemOpiton.push({
                textContent:device.label,
                value:device.deviceId
            })
        }
    });
});

2.使用 @vueuse/core 里的 useDevicesList()检查可用的输入设备

const {
    devices,
    videoInputs: cameras,
    audioInputs: microphones,
    audioOutputs: speakers,
} = useDevicesList();

/**
 * 设备ID 黑名单
 * Chrome 使用 deviceId 为 default 或 communications (Windows 设备下会有该 deviceId) 时,若插入新的麦克风,再拔出,可能会导致麦克风采集中断。
 * 规避方案:避免使用 deviceId 为 default 或 communications 的麦克风设备(SDK 枚举不对给这两个 deviceId 的设备)
 */
const deviceIdIgnores = ["default", "communications"];

// 过滤并将输入音频序列化为: [{ label: `${deviceName}`, value: `${deviceId}` }]
const soundSourceOptions = computed(() => {
    let systemOptions = microphones.value
        .filter((item) => !deviceIdIgnores.includes(item.deviceId))
        .map(({ deviceId, label }) => ({
            label,
            value: deviceId,
            show: "system",
        }));
    return systemOptions;
});

2.使用 HTML Canvas 作为源 MediaStream

在 9 秒后停止录制

const canvas = document.querySelector("canvas");

// Optional frames per second argument.
const stream = canvas.captureStream(25);
const recordedChunks = [];

console.log(stream);
const options = { mimeType: "video/webm; codecs=vp9" };
const mediaRecorder = new MediaRecorder(stream, options);

mediaRecorder.ondataavailable = handleDataAvailable;
mediaRecorder.start();

function handleDataAvailable(event) {
    console.log("data-available");
    if (event.data.size > 0) {
        recordedChunks.push(event.data);
        console.log(recordedChunks);
        download();
    } else {
        // …
    }
}
function download() {
    const blob = new Blob(recordedChunks, {
        type: "video/webm",
    });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    document.body.appendChild(a);
    a.style = "display: none";
    a.href = url;
    a.download = "test.webm";
    a.click();
    window.URL.revokeObjectURL(url);
}

// demo: to download after 9sec
setTimeout((event) => {
    console.log("stopping");
    mediaRecorder.stop();
}, 9000);

3.Blob 对象简介

https://developer.mozilla.org/en-US/docs/Web/API/Blob

4.File 对象简介

https://developer.mozilla.org/en-US/docs/Web/API/File

5.FileList 对象简介

https://developer.mozilla.org/en-US/docs/Web/API/FileList

6.FileReader 对象简介

FileReader 对象允许 Web 应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 FileBlob 对象指定要读取的文件或数据。

其中 File 对象可以是来自用户在一个 <input> 元素上选择文件后返回的 FileList 对象,也可以来自拖放操作生成的 DataTransfer 对象,还可以是来自在一个 HTMLCanvasElement 上执行 mozGetAsFile() 方法后返回结果。

重要提示: FileReader 仅用于以安全的方式从用户(远程)系统读取文件内容 它不能用于从文件系统中按路径名简单地读取文件。要在 JavaScript 中按路径名读取文件,应使用标准 Ajax 解决方案进行服务器端文件读取,如果读取跨域,则使用 CORS 权限。

属性

属性 描述
FileReader.error 返回一个 DOMError,返回读取文件时的错误信息
FileReader.result 返回文件的内容(一个字符串或者一个ArrayBuffer)。只有在读取操作完成后,此属性才有效,返回的数据的格式取决于是使用哪种读取方法来执行读取操作的
FileReader.readyState 表示FileReader 在读取操作时的当前状态。
EMPTY 0 reader 已经创建,但还没有调用任何方法。
LOADING 1 读取的方法已经被调用/数据正在被加载。
DONE 2 已完成全部的读取请求。这意味着:全部的 FileBlob 已经读到内存中,或者文件读取错误发生,或者调用了 abort(),或者取消了读取

方法

FileReader 的实例拥有 5 个方法,其中 4 个用以读取文件,另一个用来中断读取。

完成后, 5 个方法的 readyState 都会变成 DONE (已完成),并触发 loadend 事件。

下面的表格列出了这些方法以及他们的参数、功能和语法

需要注意的是 ,无论读取成功或失败,方法并不会返回读取结果,这一结果存储在 result 属性中。

方法名 描述 语法
FileReader.abort() 中止读取操作。触发之后, readyState 属性为 DONE instanceOfFileReader.abort();
FileReader.readAsArrayBuffer() 开始读取指定的 BlobFile 中的内容。 result 属性中将包含一个 ArrayBuffer 对象以表示所读取文件的数据。 instanceOfFileReader.readAsArrayBuffer(blob);
FileReader.readAsBinaryString() 开始读取指定的 BlobFile 中的内容。 result 属性将包含所读取文件原始二进制格式 instanceOfFileReader.readAsBinaryString(blob);
FileReader.readAsDataURL() 开始读取指定的 BlobFile 中的内容。 result 属性将包含一个 data:URL 格式的字符串(base64 编码)以表示所读取文件的内容。 readAsDataURL(blob)
FileReader.readAsText() 开始读取指定的 BlobFile 中的内容。 result 属性将包含所读取文件根据特殊的编码格式转化的字符串形式 instance of FileReader.readAsText(blob[, encoding]);

注意:readAsArrayBuffer() 从 2012 年 7 月 12 日起,该方法已从 W3C 工作草案废除。该特性是非标准的,请尽量不要在生产环境中使用它!

readAsText() 这个方法是异步的,只有当执行完成后才能够查看到结果,如果直接查看是无结果的,并返回 undefined。且必须要挂载实例下的 onloadonloadend 的方法处理转化后的结果。

事件处理

FileReader 包含了一套完整的事件模型,用于捕获读取文件时的状态,下面这个表格归纳了这些事件。

事件 描述 语法
FileReader.onabort 处理 abort 事件。该事件在读取操作被中断时触发。 addEventListener(“abort”, (event) => {});
onabort = (event) => {};
FileReader.onerror 处理 error 事件。该事件在读取操作发生错误时触发。 addEventListener(“error”, (event) => {});
onerror = (event) => {};
FileReader.onload 处理 load 事件。该事件在读取操作完成时触发。 addEventListener(“load”, (event) => {});
onload = (event) => {};
FileReader.onloadstart 处理 loadstart 事件。该事件在读取操作开始时触发。 addEventListener(“loadstart”, (event) => {});
onloadstart = (event) => {};
FileReader.onloadend 处理 loadend 事件。该事件在读取操作结束时(要么成功,要么失败)触发。 addEventListener(“loadend”, (event) => {});
onloadend = (event) => {};
FileReader.onprogress 处理 progress 事件。该事件在读取 Blob 时触发。 addEventListener(“progress”, (event) => {});
onprogress = (event) => {};

因为 FileReader 继承自 EventTarget ,所以所有这些事件也可以通过 addEventListener 方法使用。

检测浏览器对 FileReader 的支持

if(window.FileReader) {
    var fr = new FileReader();
    // add your code here
}else {
    alert("Not supported by your browser!");
}

7.FileReaderSync 对象简介

https://developer.mozilla.org/en-US/docs/Web/API/FileReaderSync

8.URL.createObjectURL() 对象简介

https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL

9.URL.revokeObjectURL() 对象简介

https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL

JS纯前端实现audio音频剪裁剪切复制播放与上传
https://www.zhangxinxu.com/wordpress/2020/07/js-audio-clip-copy-upload/#comments

参阅文档:

https://www.kancloud.cn/chandler/nodejs/2625641

http://chart.zhenglinglu.cn/pages/2e7a6b/#node-%E5%AE%9E%E7%8E%B0%E6%96%87%E4%BB%B6%E7%9A%84%E6%8B%B7%E8%B4%9D%E5%92%8C%E5%A4%8D%E5%88%B6


文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
  目录