MediaRecorder API 是一个强大的工具,用于从 MediaStream
(如屏幕、摄像头、麦克风等)录制视频和音频。它提供了许多属性和方法来控制录制过程,并通过事件来处理录制数据。
下面,我将重点讲解 MediaRecorder
的时间属性与 API,尤其是如何使用 timeslice
和 ondataavailable
来控制录制过程中的数据采集时间。
1. MediaRecorder
基本属性
在 MediaRecorder
中,时间相关的属性主要影响录制的数据流的获取方式,具体包括:
timeslice
(用于start()
方法)audioBitsPerSecond/videoBitsPerSecond
只读属性MediaRecorder返回正在使用的音视频编码比特率ondataavailable
(当录制的数据可用时触发)onstart
(开始录制是触发)onstop
(录制结束时触发)onpause
(录制暂停时触发)onresume
(恢复录制时触发)onerror
(方式错误触发)state
(以下三个状态之一)
inactive:未进行录制 — 要么尚未开始,要么已开始然后停止。
recording:正在捕获数据。
paused:录制已开始,然后暂停,但尚未停止或恢复。
2. start()
方法与 timeslice
start()
方法是 MediaRecorder
开始录制的核心方法,它可以接受一个 timeslice
参数,用于设置 每次触发 ondataavailable
的时间间隔。
语法:
mediaRecorder.start(timeslice);
timeslice
(可选,单位:毫秒):指定每隔多少毫秒触发一次ondataavailable
事件。在此期间录制的数据将被积累,直到触发该事件。
如何使用 timeslice
:
- 如果你希望每隔一段时间就处理录制的数据,可以设置一个较短的
timeslice
值(例如,1000 毫秒,即每秒触发一次)。 - 这个值通常用于 实时数据传输 或 实时处理 场景,比如直播流媒体、实时分析等。
示例:
每隔 1 秒获取一次数据:
let mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
// 处理录制的数据
console.log("获取到的数据:", event.data);
}
};
mediaRecorder.onstop = () => {
console.log("录制结束");
};
// 每 1 秒触发一次 ondataavailable 事件
mediaRecorder.start(1000);
在这个例子中:
- 每隔 1 秒(1000 毫秒)触发一次
ondataavailable
事件,event.data
中存储的是这 1 秒内录制的媒体数据。 mediaRecorder.onstop
会在录制结束时触发,用于处理最终录制的数据。
3. MediaRecorder.isTypeSupported(mimeType)
方法
MediaRecorder.isTypeSupported()
是 MediaRecorder
API 中的一个静态方法,用来检查浏览器是否支持指定的 MIME 类型(格式)。它返回一个布尔值,指示浏览器是否能够录制指定格式的视频或音频。
语法:
MediaRecorder.isTypeSupported(mimeType);
mimeType
:字符串类型,表示要检查的 MIME 类型,通常是视频或音频的格式,比如"video/webm"
,"audio/webm"
,"video/mp4"
,"audio/mp3"
等。- 返回值:
true
表示支持该 MIME 类型,false
表示不支持。
常见的 MIME 类型
video/webm
:WebM 格式的视频。audio/webm
:WebM 格式的音频。video/mp4
:MP4 格式的视频(通常需要额外支持)。audio/mp3
:MP3 格式的音频。video/ogg
:OGG 格式的视频。audio/ogg
:OGG 格式的音频。
如何使用 MediaRecorder.isTypeSupported()
你可以使用 isTypeSupported()
来检查浏览器是否支持特定的 MIME 类型。根据返回的布尔值,你可以选择是否使用该格式进行录制。
示例:检查浏览器是否支持 WebM 格式
if (MediaRecorder.isTypeSupported("video/webm")) {
console.log("浏览器支持 video/webm 格式");
} else {
console.log("浏览器不支持 video/webm 格式");
}
检查所有常见格式
如果你想检查浏览器支持的所有格式,可以通过遍历一个 MIME 类型列表来检查:
const mimeTypes = [
"video/webm",
"video/mp4",
"video/ogg",
"audio/webm",
"audio/mp3",
"audio/ogg",
"video/webm; codecs=vp8",
"video/webm; codecs=vp9",
"video/mp4; codecs=avc1.42E01E"
];
mimeTypes.forEach(type => {
if (MediaRecorder.isTypeSupported(type)) {
console.log(`${type} 被浏览器支持`);
} else {
console.log(`${type} 不被浏览器支持`);
}
});
3. ondataavailable
事件
ondataavailable
事件是一个非常重要的回调函数,它会在录制过程中周期性地触发,尤其是在使用了 timeslice
参数后。
语法:
mediaRecorder.ondataavailable = function(event) {
// 处理录制的数据
console.log("录制数据:", event.data);
};
event.data
是一个Blob
对象,包含了当前采集到的视频或音频数据。
ondataavailable
使用场景:
- 实时处理录制数据:比如在每次数据收集后做实时预处理、上传或保存。
- 减少内存占用:如果每次都将数据存储到
Blob
中并立刻处理(如上传至服务器),则可以避免将大量数据缓存在内存中。
4. onstop
事件
onstop
事件会在调用 mediaRecorder.stop()
后触发。此时,所有的录制数据都已经完成,可以进行处理或保存。
语法:
mediaRecorder.onstop = function() {
// 完成录制后的处理
console.log("录制停止");
const blob = new Blob(recordedChunks, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
const downloadLink = document.getElementById('downloadLink');
downloadLink.href = url;
downloadLink.download = 'recorded-video.webm';
downloadLink.style.display = 'block';
};
recordedChunks
是在ondataavailable
中积累的数据,它会在onstop
事件中合并成一个最终的视频文件。
5. timeslice
的用途
实时流处理
timeslice
参数非常适合 实时流处理 的场景。例如,你可以使用它来在录制过程中定时上传数据(如用于直播的场景),或者当你需要逐步处理数据时。
内存优化
如果不设置 timeslice
,所有的数据会积累到内存中,直到调用 stop()
方法为止,这样会占用大量内存,尤其是长时间录制时。而通过 timeslice
,每隔一段时间就获取一次数据,可以避免过多的数据占用内存。
6. MediaDevices
的用途
MediaDevices
接口提供了与媒体设备(如摄像头、麦克风和屏幕共享)交互的方法。除了常见的 getUserMedia()
方法外,它还包括其他几个有用的方法:
1. getDisplayMedia()
用于捕获屏幕、窗口或特定应用的内容流(常用于屏幕共享)。
语法:
navigator.mediaDevices.getDisplayMedia(constraints)
.then(stream => {
// 处理屏幕共享流
})
.catch(error => {
console.error("获取屏幕共享失败:", error);
});
参数:
constraints
:指定要捕获的屏幕内容(视频约束)。
常见用途:
- 实现屏幕共享功能(如视频会议、在线演示)。
- 录制或直播屏幕内容。
2. enumerateDevices()
获取用户设备(摄像头、麦克风、扬声器等)列表。
语法:
navigator.mediaDevices.enumerateDevices()
.then(devices => {
devices.forEach(device => {
console.log(`设备ID: ${device.deviceId}, 类型: ${device.kind}, 标签: ${device.label}`);
});
})
.catch(error => {
console.error("获取设备列表失败:", error);
});
返回值:
返回 MediaDeviceInfo
对象数组,每个对象包含设备信息:
deviceId
:设备的唯一标识符。kind
:设备类型(如"audioinput"
、"audiooutput"
、"videoinput"
)。label
:设备名称(需用户授权后可见)。
常见用途:
- 获取摄像头、麦克风、扬声器列表。
- 允许用户选择特定的输入/输出设备。
3. getSupportedConstraints()
返回浏览器支持的 MediaStreamConstraints
约束(如 width
、height
、frameRate
等)。
语法:
const supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
console.log(supportedConstraints);
返回值:
- 一个对象,包含所有支持的约束字段及其可用性(
true
或false
)。
常见用途:
- 检查某些约束是否被浏览器支持,避免使用不兼容的配置。
4. ondevicechange
事件
当设备(如摄像头、麦克风)插入或移除时触发。
语法:
navigator.mediaDevices.addEventListener("devicechange", () => {
console.log("媒体设备已更改");
});
常见用途:
- 监听设备变化,并动态更新设备选择列表。
适用场景总结
方法 | 作用 | 适用场景 |
---|---|---|
getUserMedia() | 获取摄像头/麦克风流 | 语音/视频通话、录音、拍照 |
getDisplayMedia() | 获取屏幕共享流 | 直播、远程协作、屏幕录制 |
enumerateDevices() | 获取设备列表 | 设备管理、用户选择摄像头或麦克风 |
getSupportedConstraints() | 获取支持的媒体约束 | 确保兼容性,避免使用不支持的参数 |
ondevicechange | 监听设备变更 | 设备插拔监听,动态更新设备选项 |
屏幕录制
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>共享屏幕录屏</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
padding: 20px;
background-color: #f4f4f4;
}
h2 {
color: #333;
}
video {
width: 80%;
max-width: 800px;
margin-top: 20px;
border: 3px solid #333;
border-radius: 8px;
background: #000;
}
button {
padding: 12px 20px;
margin: 10px;
font-size: 16px;
cursor: pointer;
border: none;
border-radius: 5px;
background-color: #007bff;
color: white;
transition: background 0.3s;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
button:hover:not(:disabled) {
background-color: #0056b3;
}
#timer {
font-size: 18px;
color: red;
margin-top: 10px;
font-weight: bold;
}
#filenameInput {
padding: 8px;
margin-top: 10px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
}
#downloadLink {
display: block;
margin-top: 15px;
font-size: 16px;
color: #28a745;
text-decoration: none;
font-weight: bold;
}
#downloadLink:hover {
color: #1e7e34;
}
</style>
</head>
<body>
<h2>共享屏幕录屏</h2>
<button id="startBtn">开始录屏</button>
<button id="stopBtn" disabled>停止录屏</button>
<button id="pauseBtn" disabled>暂停录屏</button>
<button id="resumeBtn" disabled>恢复录屏</button>
<button id="screenshotBtn" disabled>截图</button>
<br>
<input type="text" id="filenameInput" placeholder="请输入文件名" />
<select id="formatSelect">
<option value="webm">WebM</option>
<option value="mp4">MP4</option>
<option value="mkv">MKV</option>
</select>
<a id="downloadLink" style="display: none;">下载视频</a>
<p id="timer">录制时长:00:00</p>
<video id="preview" autoplay playsinline muted></video>
<canvas id="canvas" style="display: none;"></canvas>
<script>
let mediaRecorder, startTime, pauseTime = 0, timerInterval;
let recordedChunks = [];
document.getElementById("startBtn").addEventListener("click", async () => {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { cursor: "always" },
audio: { echoCancellation: true, noiseSuppression: true }
});
const preview = document.getElementById("preview");
preview.srcObject = stream;
mediaRecorder = new MediaRecorder(stream, { mimeType: "video/webm" });
recordedChunks = []; // 清空已录制的片段
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
recordedChunks.push(event.data);
}
};
mediaRecorder.onstop = () => {
const format = document.getElementById("formatSelect").value;
let mimeType = "video/webm";
let extension = "webm";
if (format === "mp4") {
mimeType = "video/mp4";
extension = "mp4";
} else if (format === "mkv") {
mimeType = "video/x-matroska";
extension = "mkv";
}
const blob = new Blob(recordedChunks, { type: mimeType });
const url = URL.createObjectURL(blob);
const downloadLink = document.getElementById("downloadLink");
let filename = document.getElementById("filenameInput").value.trim();
if (!filename) filename = "recorded-video";
downloadLink.href = url;
downloadLink.download = filename + "." + extension;
downloadLink.style.display = "block";
downloadLink.innerText = "下载录屏视频";
clearInterval(timerInterval);
document.getElementById("timer").innerText = "录制时长:00:00";
};
// 重新录制时重置时间
startTime = Date.now();
pauseTime = 0;
startTimer(); // 重新启动计时器
mediaRecorder.start();
document.getElementById("startBtn").disabled = true;
document.getElementById("stopBtn").disabled = false;
document.getElementById("pauseBtn").disabled = false;
document.getElementById("resumeBtn").disabled = true;
document.getElementById("screenshotBtn").disabled = false;
} catch (error) {
console.error("获取屏幕共享失败:", error);
}
});
document.getElementById("stopBtn").addEventListener("click", () => {
mediaRecorder.stop();
document.getElementById("preview").srcObject.getTracks().forEach(track => track.stop());
document.getElementById("startBtn").disabled = false;
document.getElementById("stopBtn").disabled = true;
document.getElementById("pauseBtn").disabled = true;
document.getElementById("resumeBtn").disabled = true;
document.getElementById("screenshotBtn").disabled = true;
});
document.getElementById("pauseBtn").addEventListener("click", () => {
if (mediaRecorder.state === "recording") {
mediaRecorder.pause();
pauseTime = Date.now(); // 记录暂停时间
clearInterval(timerInterval);
document.getElementById("pauseBtn").disabled = true;
document.getElementById("resumeBtn").disabled = false;
}
});
document.getElementById("resumeBtn").addEventListener("click", () => {
if (mediaRecorder.state === "paused") {
mediaRecorder.resume();
const resumeTime = Date.now();
startTime += resumeTime - pauseTime; // 计算暂停期间的时间差
startTimer();
document.getElementById("pauseBtn").disabled = false;
document.getElementById("resumeBtn").disabled = true;
}
});
document.getElementById("screenshotBtn").addEventListener("click", () => {
const video = document.getElementById("preview");
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const link = document.createElement("a");
link.href = canvas.toDataURL("image/png");
link.download = "screenshot.png";
link.click();
});
function startTimer() {
startTime = startTime || Date.now();
timerInterval = setInterval(() => {
const elapsedTime = Math.floor((Date.now() - startTime) / 1000);
const minutes = String(Math.floor(elapsedTime / 60)).padStart(2, "0");
const seconds = String(elapsedTime % 60).padStart(2, "0");
document.getElementById("timer").innerText = `录制时长:${minutes}:${seconds}`;
}, 1000);
}
</script>
</body>
</html>