由于本人的项目主要是 Vue3 + electron 进行开发,本文也以Vue3进行讲解:
1.使用 electron + navigator.mediaDevices.getUserMedia 录制音视频
想获取视频流,首先需要获取所需要捕获视频流的 MediaSourceId
。 Electron
提供了一个获取各个“窗口”和“屏幕”视频 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
里时候,并没有改变 recordedChunks
里 mimeType
的格式,仅仅是给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 应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File
或 Blob
对象指定要读取的文件或数据。
其中 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 已完成全部的读取请求。这意味着:全部的 File 或 Blob 已经读到内存中,或者文件读取错误发生,或者调用了 abort() ,或者取消了读取 |
方法
FileReader
的实例拥有 5 个方法,其中 4 个用以读取文件,另一个用来中断读取。
完成后, 5 个方法的 readyState
都会变成 DONE
(已完成),并触发 loadend
事件。
下面的表格列出了这些方法以及他们的参数、功能和语法
需要注意的是 ,无论读取成功或失败,方法并不会返回读取结果,这一结果存储在
result
属性中。
方法名 | 描述 | 语法 |
---|---|---|
FileReader.abort() | 中止读取操作。触发之后, readyState 属性为 DONE 。 |
instanceOfFileReader.abort(); |
FileReader.readAsArrayBuffer() | 开始读取指定的 Blob 或 File 中的内容。 result 属性中将包含一个 ArrayBuffer 对象以表示所读取文件的数据。 |
instanceOfFileReader.readAsArrayBuffer(blob); |
FileReader.readAsBinaryString() | 开始读取指定的 Blob 或 File 中的内容。 result 属性将包含所读取文件原始二进制格式。 |
instanceOfFileReader.readAsBinaryString(blob); |
FileReader.readAsDataURL() | 开始读取指定的 Blob 或 File 中的内容。 result 属性将包含一个 data:URL 格式的字符串(base64 编码 )以表示所读取文件的内容。 |
readAsDataURL(blob) |
FileReader.readAsText() | 开始读取指定的 Blob 或 File 中的内容。 result 属性将包含所读取文件根据特殊的编码格式转化的字符串形式。 |
instance of FileReader.readAsText(blob[, encoding]); |
注意:
readAsArrayBuffer()
从 2012 年 7 月 12 日起,该方法已从 W3C 工作草案废除。该特性是非标准的,请尽量不要在生产环境中使用它!
readAsText()
这个方法是异步的,只有当执行完成后才能够查看到结果,如果直接查看是无结果的,并返回undefined
。且必须要挂载实例下的onload
或onloadend
的方法处理转化后的结果。
事件处理
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
参阅文档: