当前位置: 首页 > article >正文

Python aiortc API

本研究的主要目的是基于Python aiortc api实现抓取本地设备(摄像机、麦克风)媒体流实现Web端预览。本文章仅仅描述实现思路,索要源码请私信我。

demo-server解耦

原始代码解析

http服务器端

import argparse
import asyncio
import json
import logging
import os
import ssl
import uuidimport cv2
from aiohttp import web
from aiortc import MediaStreamTrack, RTCPeerConnection, RTCSessionDescription
from aiortc.contrib.media import MediaBlackhole, MediaPlayer, MediaRecorder, MediaRelay
from av import VideoFrame
# 这行代码设置了 ROOT 变量,它代表了当前执行文件(脚本)所在的目录。
# __file__ 是 Python 中的一个特殊变量,它包含了当前文件的路径。
# os.path.dirname 函数返回路径中的目录名称。
# 这个变量通常用于构建其他文件路径,确保它们相对于当前脚本的位置。
ROOT = os.path.dirname(__file__)
# 这行代码创建了一个日志记录器(logger),它的名称是 "pc"。在 Python 的 logging 模块中,
# 每个 logger 都有一个唯一的名称,你可以使用这个名称来获取对应的 logger 实例。
# 这个 logger 将用于记录程序中与 WebRTC 相关的事件和信息。
logger = logging.getLogger("pc")
# 这行代码初始化了一个空的集合(set),名为 pcs。
# 这个集合可能用于存储 RTCPeerConnection 对象的引用。
# 使用集合可以方便地进行添加、检查和删除操作,并且集合中的元素是唯一的。
pcs = set()
# 这行代码创建了一个 MediaRelay 对象,名为 relay。MediaRelay 可能是一个用于
# 中继媒体流的自定义类,它允许将从一个 RTCPeerConnection 
# 接收到的媒体流转发到另一个 RTCPeerConnection。
# 这种类型的中继通常用于 MCU(Multipoint Control Unit)场景或简单的媒体路由。
relay = MediaRelay()# 这段代码定义了一个名为 VideoTransformTrack 的类,它是 MediaStreamTrack 的子类。
# VideoTransformTrack 类的目的是从一个已有的视频轨道(track)接收帧,
# 并对这些帧应用特定的转换(transform),
# 然后返回转换后的帧。以下是对类中各个部分的详细解释:
class VideoTransformTrack(MediaStreamTrack): # 继承"""A video stream track that transforms frames from an another track."""
# kind = "video": 这行代码设置了轨道的类型为视频。kind = "video"
# 构造函数 __init__(self, track, transform)
# track 参数是另一个 MediaStreamTrack 实例,VideoTransformTrack 将从这个轨道接收帧。
# transform 参数是一个字符串,指定要应用的转换类型,可以是 "cartoon"、"edges" 或 "rotate"。def __init__(self, track, transform):super().__init__()  # don't forget this!self.track = trackself.transform = transform# 这个方法展示了如何使用OpenCV对视频帧进行实时处理,包括卡通效果、边缘检测和旋转,
# 并将处理后的帧返回给WebRTC轨道。
# 这段代码是VideoTransformTrack类中的recv方法,它是一个异步方法,# 用于接收视频帧并根据指定的转换类型对帧进行处理。# 下面是对这个方法的详细解释:async def recv(self):# 这行代码异步地从self.track(即类的track属性,一个视频轨道)接收一个视频帧。frame = await self.track.recv()
# 如果转换类型是“cartoon”,则将接收到的视频帧转换为BGR颜色空间的NumPy数组。if self.transform == "cartoon":img = frame.to_ndarray(format="bgr24")
# 这部分代码首先对图像进行两次降采样(pyrDown),然后应用六次双边滤波(bilateralFilter),# 最后进行两次升采样(pyrUp)。# 这个过程有助于减少图像噪声并保持边缘清晰。# prepare colorimg_color = cv2.pyrDown(cv2.pyrDown(img))for _ in range(6):img_color = cv2.bilateralFilter(img_color, 9, 9, 7)img_color = cv2.pyrUp(cv2.pyrUp(img_color))
# 这部分代码将图像转换为灰度图,然后应用中值滤波(medianBlur),# 自适应阈值(adaptiveThreshold)来提取边缘,
# 最后将灰度图转换回RGB颜色空间。# prepare edgesimg_edges = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)img_edges = cv2.adaptiveThreshold(cv2.medianBlur(img_edges, 7),255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,9,2,)# 将处理过的颜色和边缘图像进行按位与操作,以结合两者。img_edges = cv2.cvtColor(img_edges, cv2.COLOR_GRAY2RGB)# combine color and edgesimg = cv2.bitwise_and(img_color, img_edges)
# 最后,将处理后的NumPy数组转换回VideoFrame对象,
# 并保留原始帧的时间戳(pts)和时间基(time_base),然后返回这个新帧。# rebuild a VideoFrame, preserving timing informationnew_frame = VideoFrame.from_ndarray(img, format="bgr24")new_frame.pts = frame.ptsnew_frame.time_base = frame.time_basereturn new_frame# 如果转换类型是“edges”,则对帧进行Canny边缘检测,并将结果转换回BGR颜色空间。elif self.transform == "edges":# perform edge detectionimg = frame.to_ndarray(format="bgr24")img = cv2.cvtColor(cv2.Canny(img, 100, 200), cv2.COLOR_GRAY2BGR)
# 然后将处理后的边缘检测图像转换回VideoFrame对象,并返回。# rebuild a VideoFrame, preserving timing informationnew_frame = VideoFrame.from_ndarray(img, format="bgr24")new_frame.pts = frame.ptsnew_frame.time_base = frame.time_basereturn new_frame# 如果转换类型是“rotate”,则将帧转换为NumPy数组,并计算旋转矩阵,# 然后应用仿射变换(warpAffine)来旋转图像。elif self.transform == "rotate":# rotate imageimg = frame.to_ndarray(format="bgr24")rows, cols, _ = img.shapeM = cv2.getRotationMatrix2D((cols / 2, rows / 2), frame.time * 45, 1)img = cv2.warpAffine(img, M, (cols, rows))
# 将旋转后的图像转换回VideoFrame对象,并返回。# rebuild a VideoFrame, preserving timing informationnew_frame = VideoFrame.from_ndarray(img, format="bgr24")new_frame.pts = frame.ptsnew_frame.time_base = frame.time_basereturn new_frameelse:# 如果转换类型既不是“cartoon”、“edges”也不是“rotate”,则直接返回原始帧。return frame# 这个函数处理对服务器根URL(通常是/)的GET请求。
# 它的作用是返回服务器根目录下index.html文件的内容作为HTTP响应。
# request:这是aiohttp传入的请求对象,包含了请求的详细信息。
# os.path.join(ROOT, "index.html"):使用os.path.join函数构建index.html文件的完整路径。
# ROOT是之前定义的服务器根目录变量。
# open(...).read():以只读模式打开index.html文件,并读取其内容。
# web.Response(content_type="text/html", text=content):创建一个aiohttp响应对象,
# 设置内容类型为text/html,并将读取的HTML内容作为响应正文返回。
async def index(request):content = open(os.path.join(ROOT, "index.html"), "r").read()return web.Response(content_type="text/html", text=content)# 这个函数处理对/client.js路径的GET请求。它的作用是返回服务器根目录下
# client.js文件的内容作为HTTP响应。
# request:这是aiohttp传入的请求对象。
# os.path.join(ROOT, "client.js"):构建client.js文件的完整路径。
# open(...).read():以只读模式打开client.js文件,并读取其内容。
# web.Response(content_type="application/javascript", text=content):
# 创建一个aiohttp响应对象,设置内容类型为application/javascript,
# 并将读取的JavaScript内容作为响应正文返回。
async def javascript(request):content = open(os.path.join(ROOT, "client.js"), "r").read()return web.Response(content_type="application/javascript", text=content)# 这个offer函数是一个异步的Web服务器路由处理函数,用于处理WebRTC连接的建立过程。
# 它接收客户端发送的offer,
# 创建或处理一个RTCPeerConnection对象,并返回一个answer给客户端。
async def offer(request):# 这行代码异步地解析客户端请求的JSON数据,通常包含SDP(会话描述协议)信息和其他参数。params = await request.json()# 使用客户端发送的SDP信息和类型创建一个RTCSessionDescription对象,这个对象表示客户端的offer。offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])# 创建一个RTCPeerConnection对象,这是WebRTC中用于管理WebRTC连接的对象。pc = RTCPeerConnection()# 为这个连接创建一个唯一的ID。pc_id = "PeerConnection(%s)" % uuid.uuid4()# 将这个连接对象添加到全局的连接集合中,以便后续可以对其进行管理。pcs.add(pc)# 定义一个日志记录函数,用于记录与特定连接相关的信息。def log_info(msg, *args):logger.info(pc_id + " " + msg, *args)# 记录创建连接的远程地址信息。log_info("Created for %s", request.remote)# 准备本地媒体# prepare local media# 创建一个MediaPlayer对象,用于播放本地音频文件。player = MediaPlayer(os.path.join(ROOT, "demo-instruct.wav"))# 根据命令行参数决定是否创建一个MediaRecorder对象来录制接收到的媒体流,# 或者使用一个MediaBlackhole对象来忽略接收到的媒体流。if args.record_to:recorder = MediaRecorder(args.record_to)else:recorder = MediaBlackhole()# 监听数据通道事件。@pc.on("datachannel")def on_datachannel(channel):# 在数据通道上监听消息事件。@channel.on("message")def on_message(message):# 如果接收到的消息是字符串并且以"ping"开头,则回复一个以"pong"开头的消息。if isinstance(message, str) and message.startswith("ping"):channel.send("pong" + message[4:])# 监听连接状态变化事件。@pc.on("connectionstatechange")async def on_connectionstatechange():log_info("Connection state is %s", pc.connectionState)# 记录连接状态。# 如果连接状态为失败,则关闭连接并从集合中移除。if pc.connectionState == "failed":await pc.close()pcs.discard(pc)@pc.on("track")def on_track(track):log_info("Track %s received", track.kind)# 监听轨道事件。# 如果接收到的是音频轨道,则将播放器的音频轨道添加到连接中,并录制接收到的音频轨道。if track.kind == "audio":pc.addTrack(player.audio)recorder.addTrack(track)# 如果接收到的是视频轨道,则创建一个VideoTransformTrack对象来处理视频,并将其添加到连接中。elif track.kind == "video":pc.addTrack(VideoTransformTrack(relay.subscribe(track), transform=params["video_transform"]))# 如果需要录制,则将接收到的视频轨道添加到录制器中。if args.record_to:recorder.addTrack(relay.subscribe(track))# 监听轨道结束事件。@track.on("ended")async def on_ended():log_info("Track %s ended", track.kind)# 当轨道结束时,停止录制。await recorder.stop()# 处理offer和发送answer# handle offer将客户端的offer设置为远程描述。await pc.setRemoteDescription(offer)# 开始录制。await recorder.start()# 创建answer。# send answeranswer = await pc.createAnswer()# 将创建的answer设置为本地描述。await pc.setLocalDescription(answer)# 将本地描述的SDP信息和类型以JSON格式返回给客户端。# 这个函数展示了如何使用aiortc库来处理WebRTC的offer/answer模型,# 包括创建连接、处理媒体流、设置事件监听器以及发送answer。return web.Response(content_type="application/json",text=json.dumps({"sdp": pc.localDescription.sdp, "type": pc.localDescription.type}),)# 这个on_shutdown函数是一个异步函数,它被设计为在Web服务器关闭时执行。
# 它的主要任务是优雅地关闭所有的RTCPeerConnection对象,并清理相关的资源。
# 这个函数接受一个参数app,它代表aiohttp的应用程序实例。这个参数在这个函数中并没有被直接使用,
# 但它是aiohttp应用程序关闭事件的一部分。
async def on_shutdown(app):# close peer connections# 这行代码创建了一个列表coros,其中包含了所有pcs集合中的RTCPeerConnection对象的关闭操作。# pc.close()是一个异步方法,用于关闭一个RTCPeerConnection对象。coros = [pc.close() for pc in pcs]# 这行代码使用asyncio.gather来并发执行所有的关闭操作。asyncio.gather是一个异步函数,# 它接受一个可迭代的异步任务列表,并返回一个代表所有任务完成的异步任务。# 这确保了所有的RTCPeerConnection对象可以同时关闭,而不是一个接一个地关闭,# 从而提高了关闭过程的效率。await asyncio.gather(*coros)# 在所有的RTCPeerConnection对象都被关闭之后,这行代码清空了pcs集合,移除了所有的连接对象引用。# 这是一个清理步骤,确保了在服务器关闭时不会有任何遗留的资源占用。pcs.clear()# 这段代码是Python脚本的入口点,它负责解析命令行参数、设置日志记录、配置SSL上下文、
# 初始化aiohttp应用,并启动Web服务器。
if __name__ == "__main__":# 创建一个ArgumentParser对象,用于解析命令行参数。描述信息说明了这个脚本# 是一个WebRTC的音频/视频/数据通道演示。parser = argparse.ArgumentParser(description="WebRTC audio / video / data-channels demo")# 添加两个命令行参数,分别用于指定SSL证书文件和密钥文件的路径。# 这些参数是可选的,用于配置HTTPS服务。parser.add_argument("--cert-file", help="SSL certificate file (for HTTPS)")parser.add_argument("--key-file", help="SSL key file (for HTTPS)")# 添加两个命令行参数,用于指定HTTP服务器的主机地址和端口号。# 默认值分别是0.0.0.0(所有可用网络接口)和8080。parser.add_argument("--host", default="0.0.0.0", help="Host for HTTP server (default: 0.0.0.0)")parser.add_argument("--port", type=int, default=8080, help="Port for HTTP server (default: 8080)")# 添加一个命令行参数,用于指定将接收到的媒体流录制到文件的路径。parser.add_argument("--record-to", help="Write received media to a file.")# 添加一个命令行参数,用于控制日志记录的详细程度。-v或--verbose可以被指定多次,以增加日志的详细程度。parser.add_argument("--verbose", "-v", action="count")# 解析命令行参数,并将解析结果存储在args对象中。args = parser.parse_args()# 根据args.verbose的值设置日志记录的级别。# 如果args.verbose为真,则设置日志级别为DEBUG,否则为INFO。if args.verbose:logging.basicConfig(level=logging.DEBUG)else:logging.basicConfig(level=logging.INFO)# SSL上下文配置# 如果用户提供了证书文件和密钥文件,则创建一个SSL上下文对象,并加载证书和密钥。# 否则,ssl_context被设置为None,表示不使用SSL。if args.cert_file:ssl_context = ssl.SSLContext()ssl_context.load_cert_chain(args.cert_file, args.key_file)else:ssl_context = None# 初始化aiohttp应用# 创建一个aiohttp应用实例。app = web.Application()# 将on_shutdown函数添加到应用的关闭事件中,以便在应用关闭时执行资源清理。app.on_shutdown.append(on_shutdown)# 为应用添加路由,分别处理根URL的GET请求(返回首页)、/client.js的# GET请求(返回客户端JavaScript代码)和/offer的POST请求(处理WebRTC offer)app.router.add_get("/", index)app.router.add_get("/client.js", javascript)app.router.add_post("/offer", offer)# 启动Web服务器# 启动aiohttp应用,设置访问日志为None(不记录访问日志),主机地址和端口号根据命令行参数设置,# 如果配置了SSL,则使用相应的SSL上下文。web.run_app(app, access_log=None, host=args.host, port=args.port, ssl_context=ssl_context)

web端

client.js
// 获取DOM元素
var dataChannelLog = document.getElementById('data-channel'), // 获取数据通道日志元素iceConnectionLog = document.getElementById('ice-connection-state'), // 获取ICE连接状态元素iceGatheringLog = document.getElementById('ice-gathering-state'), // 获取ICE收集状态元素signalingLog = document.getElementById('signaling-state'); // 获取信令状态元素// 对等连接对象
var pc = null; // 初始化对等连接对象为null// 数据通道对象
var dc = null, dcInterval = null; // 初始化数据通道对象和定时器// 创建对等连接
function createPeerConnection() {var config = {sdpSemantics: 'unified-plan' // 设置SDP语义};// 如果选中了使用STUN服务器if (document.getElementById('use-stun').checked) {config.iceServers = [{ urls: ['stun:stun.l.google.com:19302'] }]; // 配置STUN服务器}// 创建新的对等连接实例pc = new RTCPeerConnection(config);// 注册一些监听器以帮助调试pc.addEventListener('icegatheringstatechange', () => { // 当ICE收集状态改变时iceGatheringLog.textContent += ' -> ' + pc.iceGatheringState; // 更新ICE收集状态日志}, false);iceGatheringLog.textContent = pc.iceGatheringState; // 初始化ICE收集状态日志pc.addEventListener('iceconnectionstatechange', () => { // 当ICE连接状态改变时iceConnectionLog.textContent += ' -> ' + pc.iceConnectionState; // 更新ICE连接状态日志}, false);iceConnectionLog.textContent = pc.iceConnectionState; // 初始化ICE连接状态日志pc.addEventListener('signalingstatechange', () => { // 当信令状态改变时signalingLog.textContent += ' -> ' + pc.signalingState; // 更新信令状态日志}, false);signalingLog.textContent = pc.signalingState; // 初始化信令状态日志// 连接音频/视频pc.addEventListener('track', (evt) => { // 当接收到轨道时if (evt.track.kind == 'video') // 如果是视频轨道document.getElementById('video').srcObject = evt.streams[0]; // 设置视频源else // 如果是音频轨道document.getElementById('audio').srcObject = evt.streams[0]; // 设置音频源});return pc; // 返回对等连接实例
}// 枚举输入设备
function enumerateInputDevices() {const populateSelect = (select, devices) => { // 填充选择器的函数let counter = 1;devices.forEach((device) => { // 遍历设备const option = document.createElement('option'); // 创建新的选项option.value = device.deviceId; // 设置选项的值option.text = device.label || ('Device #' + counter); // 设置选项的文本select.appendChild(option); // 将选项添加到选择器counter += 1;});};navigator.mediaDevices.enumerateDevices().then((devices) => { // 枚举设备populateSelect( // 填充音频输入选择器document.getElementById('audio-input'),devices.filter((device) => device.kind == 'audioinput') // 过滤音频输入设备);populateSelect( // 填充视频输入选择器document.getElementById('video-input'),devices.filter((device) => device.kind == 'videoinput') // 过滤视频输入设备);}).catch((e) => { // 如果发生错误alert(e); // 显示错误消息});
}// 协商过程
function negotiate() {return pc.createOffer().then((offer) => { // 创建offer// [add]local offer中不存在candidate信息,也就是创建pc.setLocalDescription(offer)会自动进行candidate收集。// [add]这里异步方法等待candidate收集完毕后在发送offer。console.log('local offer: ', offer);return pc.setLocalDescription(offer); // 设置本地描述}).then(() => {// 等待ICE收集完成return new Promise((resolve) => {if (pc.iceGatheringState === 'complete') {resolve(); // 如果ICE收集已完成,解析Promise} else {function checkState() { // 检查ICE收集状态的函数if (pc.iceGatheringState === 'complete') {pc.removeEventListener('icegatheringstatechange', checkState); // 移除事件监听器resolve(); // 解析Promise}}pc.addEventListener('icegatheringstatechange', checkState); // 添加事件监听器}});}).then(() => {var offer = pc.localDescription; // 获取本地描述// [add]添加offer日志,这里的offer包含candidate信息,而在我的webrtc1v1demo代码中,offer中没有candidate信息。// [add]webrtc1v1demo中candidate信息是offer后进行交换的。console.log('send local offer: ', offer);var codec;codec = document.getElementById('audio-codec').value; // 获取音频编解码器if (codec !== 'default') { // 如果不是默认编解码器offer.sdp = sdpFilterCodec('audio', codec, offer.sdp); // 过滤SDP中的音频编解码器}codec = document.getElementById('video-codec').value; // 获取视频编解码器if (codec !== 'default') { // 如果不是默认编解码器offer.sdp = sdpFilterCodec('video', codec, offer.sdp); // 过滤SDP中的视频编解码器}document.getElementById('offer-sdp').textContent = offer.sdp; // 显示offer的SDPreturn fetch('/offer', { // 发送请求到服务器body: JSON.stringify({sdp: offer.sdp,type: offer.type,video_transform: document.getElementById('video-transform').value}),headers: {'Content-Type': 'application/json'},method: 'POST'});}).then((response) => {return response.json(); // 解析响应为JSON}).then((answer) => {document.getElementById('answer-sdp').textContent = answer.sdp; // 显示answer的SDPreturn pc.setRemoteDescription(answer); // 设置远程描述}).catch((e) => { // 如果发生错误alert(e); // 显示错误消息});
}// 开始过程
function start() {document.getElementById('start').style.display = 'none'; // 隐藏开始按钮pc = createPeerConnection(); // 创建对等连接var time_start = null; // 初始化时间戳const current_stamp = () => { // 获取当前时间戳的函数if (time_start === null) { // 如果还没有开始计时time_start = new Date().getTime(); // 开始计时return 0; // 返回0} else {return new Date().getTime() - time_start; // 返回当前时间戳}};if (document.getElementById('use-datachannel').checked) { // 如果选中了使用数据通道var parameters = JSON.parse(document.getElementById('datachannel-parameters').value); // 获取数据通道参数dc = pc.createDataChannel('chat', parameters); // 创建数据通道dc.addEventListener('close', () => { // 当数据通道关闭时clearInterval(dcInterval); // 清除定时器dataChannelLog.textContent += '- close\n'; // 更新数据通道日志});dc.addEventListener('open', () => { // 当数据通道打开时dataChannelLog.textContent += '- open\n'; // 更新数据通道日志dcInterval = setInterval(() => { // 设置定时器var message = 'ping ' + current_stamp(); // 创建ping消息dataChannelLog.textContent += '> ' + message + '\n'; // 更新数据通道日志dc.send(message); // 发送消息}, 1000); // 每秒发送一次});dc.addEventListener('message', (evt) => { // 当接收到消息时dataChannelLog.textContent += '< ' + evt.data + '\n'; // 更新数据通道日志if (evt.data.substring(0, 4) === 'pong') { // 如果是pong消息var elapsed_ms = current_stamp() - parseInt(evt.data.substring(5), 10); // 计算往返时间dataChannelLog.textContent += ' RTT ' + elapsed_ms + ' ms\n'; // 更新数据通道日志}});}// 构建媒体约束const constraints = {audio: false,video: false};if (document.getElementById('use-audio').checked) { // 如果选中了使用音频const audioConstraints = {};const device = document.getElementById('audio-input').value; // 获取音频输入设备if (device) { // 如果选择了设备audioConstraints.deviceId = { exact: device }; // 设置设备ID}constraints.audio = Object.keys(audioConstraints).length ? audioConstraints : true; // 设置音频约束}if (document.getElementById('use-video').checked) { // 如果选中了使用视频const videoConstraints = {};const device = document.getElementById('video-input').value; // 获取视频输入设备if (device) { // 如果选择了设备videoConstraints.deviceId = { exact: device }; // 设置设备ID}const resolution = document.getElementById('video-resolution').value; // 获取视频分辨率if (resolution) { // 如果设置了分辨率const dimensions = resolution.split('x'); // 分割分辨率videoConstraints.width = parseInt(dimensions[0], 0); // 设置宽度videoConstraints.height = parseInt(dimensions[1], 0); // 设置高度}constraints.video = Object.keys(videoConstraints).length ? videoConstraints : true; // 设置视频约束}// 获取媒体并开始协商if (constraints.audio || constraints.video) { // 如果需要获取媒体if (constraints.video) { // 如果需要视频document.getElementById('media').style.display = 'block'; // 显示媒体元素}navigator.mediaDevices.getUserMedia(constraints).then((stream) => { // 获取媒体stream.getTracks().forEach((track) => { // 遍历轨道pc.addTrack(track, stream); // 添加轨道到对等连接});return negotiate(); // 开始协商}, (err) => { // 如果发生错误alert('Could not acquire media: ' + err); // 显示错误消息});} else { // 如果不需要获取媒体negotiate(); // 开始协商}document.getElementById('stop').style.display = 'inline-block'; // 显示停止按钮
}// 停止过程
function stop() {document.getElementById('stop').style.display = 'none'; // 隐藏停止按钮// 关闭数据通道if (dc) { // 如果存在数据通道dc.close(); // 关闭数据通道}// 关闭传输器if (pc.getTransceivers) { // 如果对等连接支持获取传输器pc.getTransceivers().forEach((transceiver) => { // 遍历传输器if (transceiver.stop) { // 如果传输器可以停止transceiver.stop(); // 停止传输器}});}// 关闭本地音频/视频pc.getSenders().forEach((sender) => { // 遍历发送器sender.track.stop(); // 停止轨道});// 关闭对等连接setTimeout(() => { // 设置延迟pc.close(); // 关闭对等连接}, 500); // 500毫秒后关闭
}// 过滤SDP中的编解码器
function sdpFilterCodec(kind, codec, realSdp) {var allowed = []var rtxRegex = new RegExp('a=fmtp:(\\d+) apt=(\\d+)\r$');var codecRegex = new RegExp('a=rtpmap:([0-9]+) ' + escapeRegExp(codec))var videoRegex = new RegExp('(m=' + kind + ' .*?)( ([0-9]+))*\\s*$')var lines = realSdp.split('\n');var isKind = false;for (var i = 0; i < lines.length; i++) {if (lines[i].startsWith('m=' + kind + ' ')) {isKind = true;} else if (lines[i].startsWith('m=')) {isKind = false;}if (isKind) {var match = lines[i].match(codecRegex);if (match) {allowed.push(parseInt(match[1]));}match = lines[i].match(rtxRegex);if (match && allowed.includes(parseInt(match[2]))) {allowed.push(parseInt(match[1]));}}}var skipRegex = 'a=(fmtp|rtcp-fb|rtpmap):([0-9]+)';var sdp = '';isKind = false;for (var i = 0; i < lines.length; i++) {if (lines[i].startsWith('m=' + kind + ' ')) {isKind = true;} else if (lines[i].startsWith('m=')) {isKind = false;}if (isKind) {var skipMatch = lines[i].match(skipRegex);if (skipMatch && !allowed.includes(parseInt(skipMatch[2]))) {continue;} else if (lines[i].match(videoRegex)) {sdp += lines[i].replace(videoRegex, '$1 ' + allowed.join(' ')) + '\n';} else {sdp += lines[i] + '\n';}} else {sdp += lines[i] + '\n';}}return sdp;
}// 转义正则表达式字符串
function escapeRegExp(string) {return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}enumerateInputDevices(); // 枚举输入设备

解耦

设备端部署音频服务
修改说明
  1. 取消Web服务
    • 删除了 indexjavascript 函数。
    • 删除了与Web服务相关的路由设置。
  2. 修改 **offer** 方法
    • offer 方法改为 connect_to_websocket,用于连接到WebSocket服务并接收消息。
  3. 解耦Web服务到WebSocket服务
    • 将Web服务的功能解耦到WebSocket服务中,WebRTC的Web端由WebSocket服务提供(Java实现)。
  4. WebSocket客户端
    • 使用 websockets 库连接到WebSocket服务。
    • 接收WebSocket服务发送的 offer 消息,并处理 offer 消息。
    • 创建 RTCPeerConnection 对象,并设置远程描述。
    • 创建本地描述,并将其发送回WebSocket服务。
Web服务增加信令服务(Java实现)
client.js修改为ws调用
修改说明
  1. 添加 WebSocket 连接
    • connectToWebSocket() 函数中创建 WebSocket 连接,并处理 onopenonmessageonclose 事件。
  2. 修改 **start()** 函数
    • start() 函数中,通过 WebSocket 连接发送消息,而不是使用 fetch 发送 HTTP 请求。
  3. 处理 WebSocket 消息
    • connectToWebSocket() 函数中,处理从 WebSocket 服务接收到的消息,并调用 handleOffer()handleAnswer() 函数。
  4. 页面加载时连接 WebSocket
    • window.onload 事件中调用 connectToWebSocket() 函数,确保页面加载时自动连接 WebSocket 服务。

抓取本地麦克风流

未完待续…

抓取本地摄像头流

未完待续…

相关文章:

Python aiortc API

本研究的主要目的是基于Python aiortc api实现抓取本地设备&#xff08;摄像机、麦克风&#xff09;媒体流实现Web端预览。本文章仅仅描述实现思路&#xff0c;索要源码请私信我。 demo-server解耦 原始代码解析 http服务器端 import argparse import asyncio import json…...

OpenCV4,快速入门,第二讲:图像色彩空间转换

文章目录 引言一、色彩空间概述1.1 RGB与HSV的区别1.2 HSV的详细含义cvtColor二、cvtColor函数详解2.1 函数原型2.2 参数说明2.3 使用示例三、imwrite函数详解3.1 函数原型3.2 参数说明3.3 使用示例四、完整示例代码五、应用场景与注意事项5.1 HSV的典型应用5.2 注意事项结语引…...

86.(2)攻防世界 WEB PHP2

之前做过&#xff0c;回顾一遍&#xff0c;详解见下面这篇博客 29.攻防世界PHP2-CSDN博客 既然是代码审计题目&#xff0c;打开后又不显示代码&#xff0c;肯定在文件里 <?php // 首先检查通过 GET 请求传递的名为 "id" 的参数值是否严格等于字符串 "admi…...

【Leetcode 每日一题】90. 子集 II

问题背景 给你一个整数数组 n u m s nums nums&#xff0c;其中可能包含重复元素&#xff0c;请你返回该数组所有可能的 子集&#xff08;幂集&#xff09;。 解集 不能 包含重复的子集。返回的解集中&#xff0c;子集可以按 任意顺序 排列。 数据约束 ● 1 ≤ n u m s . …...

RK3588——解决Linux系统触摸屏坐标方向相反问题

问题描述&#xff1a;触摸正常产生中断&#xff0c;但系统上报的触摸坐标不正确&#xff0c;是反向的坐标。 解决办法通过修改设备树添加属性翻转坐标。 注&#xff1a;需确认对应的驱动是否有解析该属性的具体内容&#xff0c;否则仍然无法生效。...

面对全球化的泼天流量,出海企业如何观测多地域网络质量?

作者&#xff1a;俞嵩、白玙 泼天富贵背后&#xff0c;技术挑战接踵而至 随着全球化进程&#xff0c;出海、全球化成为很多 Toc 产品的必经之路&#xff0c;保障不同地域、不同网络环境的一致的用户体验成为全球化应用的不得不面对的问题。在跨运营商、跨地域的网络环境中&am…...

YOLOv11实时目标检测 | 摄像头视频图片文件检测

在上篇文章中YOLO11环境部署 || 从检测到训练https://blog.csdn.net/2301_79442295/article/details/145414103#comments_36164492&#xff0c;我们详细探讨了YOLO11的部署以及推理训练&#xff0c;但是评论区的观众老爷就说了&#xff1a;“博主博主&#xff0c;你这个只能推理…...

PyQt6/PySide6 的 QPushButton 类

QPushButton 是 PyQt6 或 PySide6 库中用于创建按钮控件的类。按钮是用户界面中最常用的控件之一&#xff0c;用于触发特定的动作或事件。QPushButton 提供了丰富的功能和灵活性&#xff0c;使得开发者可以轻松地创建各种类型的按钮。下面我将详细介绍 QPushButton 的主要特性及…...

libdrm移植到arm设备

一、环境资源要求 下载libdrm Index of /libdrm 这边使用的是2.4.114版本&#xff0c;版本太高对meson版本要求也很高&#xff0c;为了省事用apt安装meson就不用太高版本了&#xff0c;1.x版本虽然使用makefile编译方便但是太老&#xff0c;对应用支持不太好。 https://dri…...

自定义序列化数据类型

目录 1. WritableComparable1.1 Writable1.2 Comparable1.3 IntWritable 2. 自定义序列化数据类型RectangleWritable3. 矩形面积计算3.1 Map3.2 Reduce 4. 代码和结果4.1 pom.xml中依赖配置4.2 工具类util4.3 矩形面积计算4.4 结果 参考 本文引用的Apache Hadoop源代码基于Apac…...

【Linux网络编程】:URL(encode),HTTP协议,telnet工具

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;Linux网络编程 &#x1f337;追光的人&#xff0c;终会万丈光芒 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 ​ Linux网络编程笔记&#xff1a; https://mp.csdn…...

C语言基础系列【3】VSCode使用

前面我们提到过VSCode有多么的好用&#xff0c;本文主要介绍如何使用VSCode编译运行C语言代码。 安装 首先去官网&#xff08;https://code.visualstudio.com/&#xff09;下载安装包&#xff0c;点击Download for Windows 获取安装包后&#xff0c;一路点击Next就可以。 配…...

学前端框架之前,你需要先理解 MVC

MVC 软件架构设计模式鼎鼎大名&#xff0c;相信你已经听说过了&#xff0c;但你确定自己已经完全理解到 MVC 的精髓了吗&#xff1f; 如果你是新同学&#xff0c;没听过 MVC&#xff0c;那可以到网上搜一些文章来看看&#xff0c;不过你要有心理准备&#xff0c;那些文章大多都…...

Mysql:数据库

Mysql 一、数据库概念&#xff1f;二、MySQL架构三、SQL语句分类四、数据库操作4.1 数据库创建4.2 数据库字符集和校验规则4.3 数据库修改4.4 数据库删除4.4 数据库备份和恢复其他 五、表操作5.1 创建表5.2 修改表5.3 删除表 六、表的增删改查6.1 Create(创建):数据新增1&#…...

python的函数介绍

一.定义和调用函数 1.定义函数 在 Python 中&#xff0c;使用 def 关键字来定义一个函数。函数可以包含参数&#xff0c;也可以包含返回值 基本语法 def function_name(parameters):"""docstring"""# Function bodyreturn resultdef greet(n…...

要完成使用MLflow比较模型运行、选择模型并将其部署到REST API的教程

要完成使用MLflow比较模型运行、选择模型并将其部署到REST API的教程&#xff0c;请按照以下有序步骤操作&#xff1a; 设置环境 导出MLflow跟踪URI&#xff1a;设置环境变量以指向您的MLflow跟踪服务。export MLFLOW_TRACKING_URIyour-organizations-MLflow-server-url 加载数…...

Windows Docker笔记-简介摘录

Docker是一个开源的容器化平台&#xff0c;可以帮助开发人员将应用程序与其依赖项打包在一个独立的容器中&#xff0c;然后在任何安装的Docker的环境中快速、可靠地运行。 几个基本概念和优势&#xff1a; 容器&#xff1a;容器是一个轻量级、独立的运行环境&#xff0c;包含了…...

MVC 文件夹:架构之美与实际应用

MVC 文件夹:架构之美与实际应用 引言 MVC(Model-View-Controller)是一种设计模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种架构模式不仅提高了代码的可维护性和可扩展性,而且使得开发流程更加清晰。本文将深入探讨MVC文…...

AI透明化与全球政治格局的发展:如何避免AI被人为操控

在现代社会&#xff0c;人工智能&#xff08;AI&#xff09;已经逐渐渗透到我们的日常生活中&#xff0c;尤其是在社交平台和信息传播领域。虽然AI可以极大地推动社会发展&#xff0c;但也潜藏着被恶意操控的风险。最令人担忧的是&#xff0c;某些势力可能通过操控AI来操控公众…...

ubuntu 网络管理--wpa_supplicant、udhcpc

ubuntu 网络管理--wpa_supplicant 1 介绍wpa_supplicant 无线认证wpa_passphrase 配置工具 NetworkManager 网络管理udhcpc 与 dhclient对比dhclient概述主要功能 udhcpc概述主要功能 2 联系依赖关系配置文件 3 区别4 如何选择5 示例使用 wpa_supplicant 手动连接无线网络使用 …...

熟练掌握Http协议

目录 基本概念请求数据Get请求方式和Post请求方式 响应数据响应状态码 基本概念 Http协议全称超文本传输协议(HyperText Transfer Protocol)&#xff0c;是网络通信中应用层的协议&#xff0c;规定了浏览器和web服务器数据传输的格式和规则 Http应用层协议具有以下特点&#…...

C++的 I/O 流

本文把复杂的基类和派生类的作用和关系捋出来&#xff0c;具体的接口请参考相关文档 C的 I/O 流相关的类&#xff0c;继承关系如下图所示 https://zh.cppreference.com/w/cpp/io I / O 的概念&#xff1a;内存和外设进行数据交互称为 I / O &#xff0c;例如&#xff1a;把数…...

寒假刷题Day22

一、2570. 合并两个二维数组 - 求和法 class Solution { public:vector<vector<int>> mergeArrays(vector<vector<int>>& nums1, vector<vector<int>>& nums2) {int i 0, j 0, n1 nums1.size(), n2 nums2.size();int tmp 0;…...

【PDF多区域识别】如何批量PDF指定多个区域识别改名,基于Windows自带的UWP的文字识别实现方案

海关在对进口货物进行查验时,需要核对报关单上的各项信息。对报关单 PDF 批量指定区域识别改名后,海关工作人员可以更高效地从文件名中获取关键信息,如货物来源地、申报价值等。例如文件名 “[原产国]_[申报价值].pdf”,有助于海关快速筛选重点查验对象,提高查验效率和监管…...

【大数据技术】本机PyCharm远程连接虚拟机Python

本机PyCharm远程连接虚拟机Python 注意:本文需要使用PyCharm专业版。 pycharm-professional-2024.1.4VMware Workstation Pro 16CentOS-Stream-10-latest-x86_64-dvd1.iso写在前面 本文主要介绍如何使用本地PyCharm远程连接虚拟机,运行Python脚本,提高编程效率。 注意: …...

Windows图形界面(GUI)-QT-C/C++ - Qt Combo Box

公开视频 -> 链接点击跳转公开课程博客首页 -> ​​​链接点击跳转博客主页 目录 一、概述 1.1 基本概念 1.2 应用场景对比 二、核心属性详解 2.1 行为控制 2.2 显示配置 三、数据操作与访问 3.1 基础数据管理 3.2 高级数据访问 四、用户交互处理 4.1 信号处…...

数字化转型:概念性名词浅谈(第四讲)

​大家好&#xff0c;本篇文章是在新年之际写的&#xff0c;所以在这里先给大家拜个年。 今天要介绍的名词为ETL: ETL&#xff0c;是英文Extract-Transform-Load的缩写&#xff0c;用来描述将数据从来源端经过抽取&#xff08;extract&#xff09;、转换&#xff08;transfor…...

标题:市场波动中的价格趋势分析与策略优化

在市场投资中&#xff0c;价格的波动不仅是常态&#xff0c;更是影响投资决策的关键因素。市场价格的变化受多种因素影响&#xff0c;包括供需关系、政策调整、国际市场动态等。如何理解价格走势&#xff0c;并在其中寻找合适的交易机会&#xff0c;是投资者需要长期研究的课题…...

【HarmonyOS之旅】基于ArkTS开发(三) -> 兼容JS的类Web开发(四) -> 常见组件(一)

目录 1 -> List 1.1 -> 创建List组件 1.2 -> 添加滚动条 1.3 -> 添加侧边索引栏 1.4 -> 实现列表折叠和展开 1.5 -> 场景示例 2 -> dialog 2.1 -> 创建Dialog组件 2.2 -> 设置弹窗响应 2.3 -> 场景示例 3 -> form 3.1 -> 创建…...

Linux:文件系统(软硬链接)

目录 inode ext2文件系统 Block Group 超级块&#xff08;Super Block&#xff09; GDT&#xff08;Group Descriptor Table&#xff09; 块位图&#xff08;Block Bitmap&#xff09; inode位图&#xff08;Inode Bitmap&#xff09; i节点表&#xff08;inode Tabl…...