基于 uniapp 开发 android 播放 webrtc 流

news2025/1/23 7:07:32

一、播放rtsp协议流
如果 webrtc 流以 rtsp 协议返回,流地址如:rtsp://127.0.0.1:5115/session.mpg,uniapp的 <video> 编译到android上直接就能播放,但通常会有2-3秒的延迟。

二、播放webrtc协议流
如果 webrtc 流以 webrtc 协议返回,流地址如:webrtc://127.0.0.1:1988/live/livestream,我们需要通过sdp协商、连接推流服务端、搭建音视频流通道来播放音视频流,通常有500毫秒左右的延迟。

封装 WebrtcVideo 组件

<template>
	<video id="rtc_media_player" width="100%" height="100%" autoplay playsinline></video>
</template>

<!-- 因为我们使用到 js 库,所以需要使用 uniapp 的 renderjs -->
<script module="webrtcVideo" lang="renderjs">
	import $ from "./jquery-1.10.2.min.js";
	import {prepareUrl} from "./utils.js";
	
	export default {
	    data() {
	        return {
	        	//RTCPeerConnection 对象
	            peerConnection: null,
	            //需要播放的webrtc流地址
	            playUrl: 'webrtc://127.0.0.1:1988/live/livestream'
	        }
	    },
	    methods: {
	          createPeerConnection() {
		      	const that = this
		      	//创建 WebRTC 通信通道
	            that.peerConnection = new RTCPeerConnection(null);
	            //添加一个单向的音视频流收发器
				that.peerConnection.addTransceiver("audio", { direction: "recvonly" });
				that.peerConnection.addTransceiver("video", { direction: "recvonly" });
				//收到服务器码流,将音视频流写入播放器
	            that.peerConnection.ontrack = (event) => {
	                const remoteVideo = document.getElementById("rtc_media_player");
	                if (remoteVideo.srcObject !== event.streams[0]) {
	                    remoteVideo.srcObject = event.streams[0];
	                }
	            };
	        },
	        async makeCall() {
				const that = this
		        const url = this.playUrl
	            this.createPeerConnection()
	            //拼接服务端请求地址,如:http://192.168.0.1:1988/rtc/v1/play/
	            const conf = prepareUrl(url);
	            //生成 offer sdp
	            const offer = await this.peerConnection.createOffer();
	            await this.peerConnection.setLocalDescription(offer);
	            var session = await new Promise(function (resolve, reject) {
		            $.ajax({
		               type: "POST",
		               url: conf.apiUrl,
		               data: offer.sdp,
		               contentType: "text/plain",
		               dataType: "json",
		               crossDomain: true,
		           })
		           .done(function (data) {
		           	   //服务端返回 answer sdp
		               if (data.code) {
							reject(data);
							return;
		            	}
		                resolve(data);
		            })
		            .fail(function (reason) {
						reject(reason);
		            });
	            });
	            //设置远端的描述信息,协商sdp,通过后搭建通道成功
	            await this.peerConnection.setRemoteDescription(
	           		new RTCSessionDescription({ type: "answer", sdp: session.sdp })
	         	);
				session.simulator = conf.schema + '//' + conf.urlObject.server + ':' + conf.port + '/rtc/v1/nack/'
				return session;
	        }
	    },
	       mounted() {
	           try {
				this.makeCall().then((res) => {
					// webrtc 通道建立成功
				})
			   } catch (error) {
				   // webrtc 通道建立失败
				   console.log(error)
			   }
	       }
	}
</script>

utils.js

const defaultPath = "/rtc/v1/play/";

export const prepareUrl = webrtcUrl => {
	var urlObject = parseUrl(webrtcUrl);
	var schema = "http:";
	var port = urlObject.port || 1985;
	if (schema === "https:") {
		port = urlObject.port || 443;
	}

	// @see https://github.com/rtcdn/rtcdn-draft
	var api = urlObject.user_query.play || defaultPath;
	if (api.lastIndexOf("/") !== api.length - 1) {
		api += "/";
	}

	apiUrl = schema + "//" + urlObject.server + ":" + port + api;
	for (var key in urlObject.user_query) {
		if (key !== "api" && key !== "play") {
			apiUrl += "&" + key + "=" + urlObject.user_query[key];
		}
	}
	// Replace /rtc/v1/play/&k=v to /rtc/v1/play/?k=v
	var apiUrl = apiUrl.replace(api + "&", api + "?");

	var streamUrl = urlObject.url;

	return {
		apiUrl: apiUrl,
		streamUrl: streamUrl,
		schema: schema,
		urlObject: urlObject,
		port: port,
		tid: Number(parseInt(new Date().getTime() * Math.random() * 100))
			.toString(16)
			.substr(0, 7)
	};
};
export const parseUrl = url => {
	// @see: http://stackoverflow.com/questions/10469575/how-to-use-location-object-to-parse-url-without-redirecting-the-page-in-javascri
	var a = document.createElement("a");
	a.href = url
		.replace("rtmp://", "http://")
		.replace("webrtc://", "http://")
		.replace("rtc://", "http://");

	var vhost = a.hostname;
	var app = a.pathname.substr(1, a.pathname.lastIndexOf("/") - 1);
	var stream = a.pathname.substr(a.pathname.lastIndexOf("/") + 1);

	// parse the vhost in the params of app, that srs supports.
	app = app.replace("...vhost...", "?vhost=");
	if (app.indexOf("?") >= 0) {
		var params = app.substr(app.indexOf("?"));
		app = app.substr(0, app.indexOf("?"));

		if (params.indexOf("vhost=") > 0) {
			vhost = params.substr(params.indexOf("vhost=") + "vhost=".length);
			if (vhost.indexOf("&") > 0) {
				vhost = vhost.substr(0, vhost.indexOf("&"));
			}
		}
	}

	// when vhost equals to server, and server is ip,
	// the vhost is __defaultVhost__
	if (a.hostname === vhost) {
		var re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
		if (re.test(a.hostname)) {
			vhost = "__defaultVhost__";
		}
	}

	// parse the schema
	var schema = "rtmp";
	if (url.indexOf("://") > 0) {
		schema = url.substr(0, url.indexOf("://"));
	}

	var port = a.port;
	if (!port) {
		if (schema === "http") {
			port = 80;
		} else if (schema === "https") {
			port = 443;
		} else if (schema === "rtmp") {
			port = 1935;
		}
	}

	var ret = {
		url: url,
		schema: schema,
		server: a.hostname,
		port: port,
		vhost: vhost,
		app: app,
		stream: stream
	};
	fill_query(a.search, ret);

	// For webrtc API, we use 443 if page is https, or schema specified it.
	if (!ret.port) {
		if (schema === "webrtc" || schema === "rtc") {
			if (ret.user_query.schema === "https") {
				ret.port = 443;
			} else if (window.location.href.indexOf("https://") === 0) {
				ret.port = 443;
			} else {
				// For WebRTC, SRS use 1985 as default API port.
				ret.port = 1985;
			}
		}
	}

	return ret;
};
export const fill_query = (query_string, obj) => {
	// pure user query object.
	obj.user_query = {};

	if (query_string.length === 0) {
		return;
	}

	// split again for angularjs.
	if (query_string.indexOf("?") >= 0) {
		query_string = query_string.split("?")[1];
	}

	var queries = query_string.split("&");
	for (var i = 0; i < queries.length; i++) {
		var elem = queries[i];

		var query = elem.split("=");
		obj[query[0]] = query[1];
		obj.user_query[query[0]] = query[1];
	}

	// alias domain for vhost.
	if (obj.domain) {
		obj.vhost = obj.domain;
	}
};

页面中使用

<template>
	<VideoWebrtc />
</template>
<script setup>
	import VideoWebrtc from "@/components/videoWebrtc";
</script>

需要注意的事项:
1.spd 协商的重要标识之一为媒体描述: m=xxx <type> <code>,示例行如下:

在这里插入图片描述

一个完整的媒体描述,从第一个m=xxx <type> <code>开始,到下一个m=xxx <type> <code>结束,以video为例,媒体描述包含了当前设备允许播放的视频流编码格式,常见如:VP8/VP9/H264 等:

在这里插入图片描述
在这里插入图片描述

对照 m=video 后边的编码发现,其包含所有 a=rtpmap 后的编码,a=rtpmap 编码后的字符串代表视频流格式,但视频编码与视频流格式却不是固定的匹配关系,也就是说,在设备A中,可能存在 a=rtpmap:106 H264/90000 表示h264,在设备B中,a=rtpmap:100 H264/90000 表示h264。

因此,如果要鉴别设备允许播放的视频流格式,我们需要观察 a=rtpmap code 后的字符串。

协商通过的部分标准为:

  1. offer sdp 的 m=xxx 数量需要与 answer sdp 的 m=xxx 数量保持一致;
  2. offer sdp 的 m=xxx 顺序需要与 answer sdp 的 m=xxx 顺序保持一致;如两者都需要将 m=audio 放在第一位,m=video放在第二位,或者反过来;
  3. answer sdp 返回的 m=audio 后的 <code>,需要被包含在 offer sdp 的 m=audio 后的<code>中;

offer sdp 的 m=xxx 由 addTransceiver 创建,首个参数为 audio 时,生成 m=audio,首个参数为video时,生成 m=video ,创建顺序对应 m=xxx 顺序

"recvonly" }); that.peerConnection.addTransceiver("video", {
direction: "recvonly" }); ```
  1. 在 sdp 中存在一项 a=mid:xxx xxx在浏览器中可能为 audiovideo ,在 android 设备上为 01,服务端需注意与 offer sdp 匹配。
  2. 关于音视频流收发器,上面使用的api是 addTransceiver ,但在部分android设备上会提示没有这个api,我们可以替换为 getUserMedia + addTrack
data() {
	return {
		......
	    localStream: null,
	    ......
	}
},
methods: {
	createPeerConnection() {
		const that = this
		//创建 WebRTC 通信通道
	    that.peerConnection = new RTCPeerConnection(null);
	    that.localStream.getTracks().forEach((track) => {
          that.peerConnection.addTrack(track, that.localStream);
        });
        //收到服务器码流,将音视频流写入播放器
	    that.peerConnection.ontrack = (event) => {
	    	......
	    };
	}async makeCall() {
		const that = this
		that.localStream = await navigator.mediaDevices.getUserMedia({
	    	video: true,
	        audio: true,
	    });
		const url = this.playUrl
		......
		......
	}
}

需要注意的是,navigator.mediaDevices.getUserMedia
获取的是设备摄像头、录音的媒体流,所以设备首先要具备摄像、录音功能,并开启对应权限,否则 api 将调用失败。

三、音视频实时通讯
这种 p2p 场景的流播放,通常需要使用 websocket 建立服务器连接,然后同时播放本地、服务端的流。

<template>
	<div>Local Video</div>
	<video id="localVideo" autoplay playsinline></video>
	<div>Remote Video</div>
	<video id="remoteVideo" autoplay playsinline></video>
</template>
<script module="webrtcVideo" lang="renderjs">
	import $ from "./jquery-1.10.2.min.js";
	export default {
		data() {
	        return {
	            signalingServerUrl: "ws://127.0.0.1:8085",
	            iceServersUrl: 'stun:stun.l.google.com:19302',
	            localStream: null,
	            peerConnection: null
	        }
	    },
	     methods: {
	     	async startLocalStream(){
		     	try {
		     		this.localStream = await navigator.mediaDevices.getUserMedia({
            			video: true,
            			audio: true,
          			});
          			document.getElementById("localVideo").srcObject = this.localStream;
		     	}catch (err) {
		     		console.error("Error accessing media devices.", err);
		     	}
	     	},
	     	createPeerConnection() {
	     		const configuration = { iceServers: [{ 
	     			urls: this.iceServersUrl 
	     		}]};
	     		this.peerConnection = new RTCPeerConnection(configuration);
	     		this.localStream.getTracks().forEach((track) => {
          			this.peerConnection.addTrack(track, this.localStream);
        		});
        		this.peerConnection.onicecandidate = (event) => {
          			if (event.candidate) {
			            ws.send(
			              JSON.stringify({
			                type: "candidate",
			                candidate: event.candidate,
			              })
			            );
			         }
        		};
        		this.peerConnection.ontrack = (event) => {
		          const remoteVideo = document.getElementById("remoteVideo");
		          if (remoteVideo.srcObject !== event.streams[0]) {
		            remoteVideo.srcObject = event.streams[0];
		          }
        		};
	     	}async makeCall() {
	     		this.createPeerConnection();
	     		const offer = await this.peerConnection.createOffer();
	     		await this.peerConnection.setLocalDescription(offer);
	     		ws.send(JSON.stringify(offer));
	     	}
	     },
	     mounted() {
	     	this.makeCall()
	     	const ws = new WebSocket(this.signalingServerUrl);
	     	 ws.onopen = () => {
		     	console.log("Connected to the signaling server");
		        this.startLocalStream();
      		};
      		ws.onmessage = async (message) => {
      			const data = JSON.parse(message.data);
      			if (data.type === "offer") {
          			if (!this.peerConnection) createPeerConnection();
          			await this.peerConnection.setRemoteDescription(
            			new RTCSessionDescription(data)
          			);
          			const answer = await this.peerConnection.createAnswer();
          			await this.peerConnection.setLocalDescription(answer);
          			ws.send(JSON.stringify(this.peerConnection.localDescription));
        		} else if (data.type === "answer") {
          			if (!this.peerConnection) createPeerConnection();
          				await this.peerConnection.setRemoteDescription(
            				new RTCSessionDescription(data)
          				);
        			} else if (data.type === "candidate") {
          				if (this.peerConnection) {
				            try {
				              await this.peerConnection.addIceCandidate(
				                new RTCIceCandidate(data.candidate)
				              );
				            } catch (e) {
				              console.error("Error adding received ICE candidate", e);
				            }
          				}
        			}
      			}
	     }
	}
</script>

与播放webrtc协议流相比,p2p 以 WebSocket 替代 ajax 实现 sdp 的发送与接收,增加了本地流的播放功能,其他与播放协议流的代码一致。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2262513.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Meta重磅发布Llama 3.3 70B:开源AI模型的新里程碑

在人工智能领域&#xff0c;Meta的最新动作再次引起了全球的关注。今天&#xff0c;我们见证了Meta发布的Llama 3.3 70B模型&#xff0c;这是一个开源的人工智能模型&#xff0c;它不仅令人印象深刻&#xff0c;而且在性能上达到了一个新的高度。 一&#xff0c;技术突破&#…

游戏AI实现-寻路算法(DFS)

​深度优先搜索算法&#xff08;英语&#xff1a;Depth-First-Search&#xff0c;缩写为DFS&#xff09;是一种用于遍历或搜索树或图的算法。 寻路地图搭建&#xff1a; 游戏AI实现-寻路地图搭建-CSDN博客 算法过程&#xff1a;遍历方向为从竖直向上沿顺时针方向 1.首先将开…

概率论得学习和整理30: 用EXCEL 描述泊松分布 poisson distribution

目录 1 泊松分布的基本内容 1.1 泊松分布的关键点 1.1.1 属于离散分布 1.1.2 泊松分布的特点&#xff1a;每个子区间内概率相等 &#xff0c; λ就是平均概率 1.2 核心参数 1.3 pmf公式 1.4 期望和方差 2 例1&#xff1a;用EXCEL计算泊松分布的概率 3 比较λ不同值时…

八、测试-性能测试

文章目录 前言一、性能测试介绍1. 简介2. 流程3. 指标4. 测试方案5. 性能评估6. 常见性能问题及解决对策 二、测试工具1. Jmeter简介2. Jmeter常见测试框架 三、Jmeter录制脚本1. 基本2. 增强3. 脚本参数化4. 断言5. 关联6. JDBC请求 四、分布式测试五、性能测试报告 前言 性能…

Open-Source Test Automation Tools for Windows Desktop Apps 2022

Do you have a Windows desktop application that needs to be tested to verify if all the different features work seamlessly and according to documentation? We suggest you use test automation—or at least try it in combination with manual testing. Test auto…

【小沐学GIS】基于C++绘制三维数字地球Earth(OpenGL、glfw、glut、QT)第三期

&#x1f37a;三维数字地球系列相关文章如下&#x1f37a;&#xff1a;1【小沐学GIS】基于C绘制三维数字地球Earth&#xff08;456:OpenGL、glfw、glut&#xff09;第一期2【小沐学GIS】基于C绘制三维数字地球Earth&#xff08;456:OpenGL、glfw、glut&#xff09;第二期3【小沐…

【论文笔记】Editing Models with Task Arithmetic

&#x1f34e;个人主页&#xff1a;小嗷犬的个人主页 &#x1f34a;个人网站&#xff1a;小嗷犬的技术小站 &#x1f96d;个人信条&#xff1a;为天地立心&#xff0c;为生民立命&#xff0c;为往圣继绝学&#xff0c;为万世开太平。 基本信息 标题: Editing Models with Task…

c++ [eigen库配置和使用]

实验环境 eigen 3.40 下载链接 https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.zip Visual Studio 2022配置 解压eigen后&#xff0c;在项目中配置包含目录 代码示例 加头文件 #include <Eigen/Dense> using namespace Eigen;矩阵运算 MatrixXd …

SpringBoot完整技术汇总

SpringBoot 注意&#xff1a;SpringBoot技术示例中的项目均已上传至Gitee&#xff0c;均可通过此处自行下载 SpringBoot是由Pivotal团队提供的全新框架&#xff0c;其设计目的是用来简化Spring应用的初始搭建以及开发过程 Spring程序与SpringBoot程序对比如下 Spring程序配置…

windows 使用python共享网络给另外一个网卡

# -*- coding: utf-8 -*- import subprocessdef open_share(to_shared_adapter, from_shared_adapter):"""打开以太网的网络共享:return: None"""powershell_script f"""# Register the HNetCfg library (once)# regsvr32 hnetc…

redis集群 服务器更换ip,怎么办,怎么更换redis集群的ip

redis集群 服务器更换ip&#xff0c;怎么办&#xff0c;怎么更换redis集群的ip 1、安装redis三主三从集群2、正常状态的redis集群3、更改redis集群服务器的ip 重启服务器 集群会down4、更改redis集群服务器的ip 重启服务器 集群down的原因5、更改redis集群服务器的ip后&#xf…

Linux入门攻坚——42、Nginx及web站点架构模式

对于lvs集群&#xff0c;是一个四层路由的集群&#xff0c;Director无需启用对端口的监控&#xff0c;直接将报文转发给后端业务服务器RealServer。 使用Nginx也可以实现集群功能&#xff0c;Nginx实现反向代理&#xff0c;实现的是七层上的转发&#xff0c;要求Nginx本身就是…

Git merge 和 rebase的区别(附图)

在 Git 中&#xff0c;merge 和 rebase 是两种用于整合分支变化的方法。虽然它们都可以将一个分支的更改引入到另一个分支中&#xff0c;但它们的工作方式和结果是不同的。以下是对这两者的详细解释&#xff1a; Git Merge 功能&#xff1a;合并分支&#xff0c;将两个分支的…

密码编码学与网络安全(第五版)答案

通过如下代码分别统计一个字符的频率和三个字符的频率&#xff0c;"8"——"e"&#xff0c;“&#xff1b;48”——“the”&#xff0c;英文字母的相对使用频率&#xff0c;猜测频率比较高的依此为&#xff09;&#xff0c;t,*,5&#xff0c;分别对应s,o,n,…

我在广州学 Mysql 系列之 数据类型和运算符详解

ℹ️大家好&#xff0c;我是&#x1f606;练小杰&#xff0c;今天主要学习 Mysql的数据类型以及运算符操作~~ 上周五学习了“Mysql 系列之 数据“表”的基本操作”~ 想要了解更多&#x1f236;️MYSQL 数据库的命令行总结&#xff01;&#xff01;&#xff01; “我是你的敌人,…

SpringBoot中基于JWt的授权与续期方案

一、 SpringBoot中Token登录授权、续期和终止的方案RedisToken SpringBoot项目写登录注册之类的方案 使用Cookie或Session的话&#xff0c;它是有状态的&#xff0c;不符合分布式技术架构使用Security或者Shiro框架实现起来比较复杂&#xff0c;一般项目无需用那么复杂使用JW…

小程序快速实现大模型聊天机器人

需求分析&#xff1a; 基于大模型&#xff0c;打造一个聊天机器人&#xff1b;使用开放API快速搭建&#xff0c;例如&#xff1a;讯飞星火&#xff1b;先实现UI展示&#xff0c;在接入API。 最终实现效果如下&#xff1a; 一.聊天机器人UI部分 1. 创建微信小程序&#xff0c…

【OSS】php使用oss存储

阿里云oss官方文档&#xff1a;文档 1、前期工作 创建阿里云账号&#xff0c;登录创建bucket&#xff0c;注意修改权限&#xff0c;要不然可能读取不到 申请accessKeyId和accessKeySecret accessKey 2、项目中安装OSS扩展 composer require aliyuncs/oss-sdk-php3、基础使…

Elasticsearch02-安装7.x

零、文章目录 Elasticsearch02-安装7.x 1、Windows安装Elasticsearch &#xff08;1&#xff09;JDK安装 Elasticsearch是基于java开发的&#xff0c;所以需要安装JDK。我们安装的Elasticsearch版本是7.15&#xff0c;对应JDK至少1.8版本以上。也可以不安装jdk&#xff0c;…

【Qt】drawText字体大小问题探究

背景 软件的一个功能是&#xff1a; 打开图片在图片上绘制序号&#xff0c;序号的样式是圆圈内包含数字将带有序号的图片打印出来 实现思路也很简单&#xff0c;在屏幕上显示时重写paintEvent函数&#xff0c;利用QPainter完成图片和序号的绘制。打印时只需要将QPainter对应…