Browse Source

流服务,转码服务,前端简单的嵌入实现了,后续需要完善很多功能。

caoyang 6 months ago
parent
commit
d7cc347323
4 changed files with 3881 additions and 0 deletions
  1. 2 0
      index.html
  2. 3514 0
      public/adapter.min.js
  3. 313 0
      public/webrtcstreamer.js
  4. 52 0
      src/views/camera/index.vue

+ 2 - 0
index.html

@@ -13,6 +13,8 @@
       name="description"
       content="管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
     />
+    <script src="/adapter.min.js"></script> 
+    <script src="/webrtcstreamer.js"></script> 
     <title>%VITE_APP_TITLE%</title>
   </head>
   <body>

File diff suppressed because it is too large
+ 3514 - 0
public/adapter.min.js


+ 313 - 0
public/webrtcstreamer.js

@@ -0,0 +1,313 @@
+var WebRtcStreamer = (function() {
+
+/** 
+ * Interface with WebRTC-streamer API
+ * @constructor
+ * @param {string} videoElement - id of the video element tag
+ * @param {string} srvurl -  url of webrtc-streamer (default is current location)
+*/
+var WebRtcStreamer = function WebRtcStreamer (videoElement, srvurl) {
+	if (typeof videoElement === "string") {
+		this.videoElement = document.getElementById(videoElement);
+	} else {
+		this.videoElement = videoElement;
+	}
+	this.srvurl           = srvurl || location.protocol+"//"+window.location.hostname+":"+window.location.port;
+	this.pc               = null;    
+
+	this.mediaConstraints = { offerToReceiveAudio: true, offerToReceiveVideo: true };
+
+	this.iceServers = null;
+	this.earlyCandidates = [];
+}
+
+WebRtcStreamer.prototype._handleHttpErrors = function (response) {
+    if (!response.ok) {
+        throw Error(response.statusText);
+    }
+    return response;
+}
+
+/** 
+ * Connect a WebRTC Stream to videoElement 
+ * @param {string} videourl - id of WebRTC video stream
+ * @param {string} audiourl - id of WebRTC audio stream
+ * @param {string} options  -  options of WebRTC call
+ * @param {string} stream   -  local stream to send
+ * @param {string} prefmime -  prefered mime
+*/
+WebRtcStreamer.prototype.connect = function(videourl, audiourl, options, localstream, prefmime) {
+	this.disconnect();
+	
+	// getIceServers is not already received
+	if (!this.iceServers) {
+		console.log("Get IceServers");
+		
+		fetch(this.srvurl + "/api/getIceServers")
+			.then(this._handleHttpErrors)
+			.then( (response) => (response.json()) )
+			.then( (response) =>  this.onReceiveGetIceServers(response, videourl, audiourl, options, localstream, prefmime))
+			.catch( (error) => this.onError("getIceServers " + error ))
+				
+	} else {
+		this.onReceiveGetIceServers(this.iceServers, videourl, audiourl, options, localstream, prefmime);
+	}
+}
+
+/** 
+ * Disconnect a WebRTC Stream and clear videoElement source
+*/
+WebRtcStreamer.prototype.disconnect = function() {		
+	if (this.videoElement?.srcObject) {
+		this.videoElement.srcObject.getTracks().forEach(track => {
+			track.stop()
+			this.videoElement.srcObject.removeTrack(track);
+		});
+	}
+	if (this.pc) {
+		fetch(this.srvurl + "/api/hangup?peerid=" + this.pc.peerid)
+			.then(this._handleHttpErrors)
+			.catch( (error) => this.onError("hangup " + error ))
+
+		
+		try {
+			this.pc.close();
+		}
+		catch (e) {
+			console.log ("Failure close peer connection:" + e);
+		}
+		this.pc = null;
+	}
+}    
+
+/*
+* GetIceServers callback
+*/
+WebRtcStreamer.prototype.onReceiveGetIceServers = function(iceServers, videourl, audiourl, options, stream, prefmime) {
+	this.iceServers       = iceServers;
+	this.pcConfig         = iceServers || {"iceServers": [] };
+	try {            
+		this.createPeerConnection();
+
+		var callurl = this.srvurl + "/api/call?peerid=" + this.pc.peerid + "&url=" + encodeURIComponent(videourl);
+		if (audiourl) {
+			callurl += "&audiourl="+encodeURIComponent(audiourl);
+		}
+		if (options) {
+			callurl += "&options="+encodeURIComponent(options);
+		}
+		
+		if (stream) {
+			this.pc.addStream(stream);
+		}
+
+                // clear early candidates
+		this.earlyCandidates.length = 0;
+		
+		// create Offer
+		this.pc.createOffer(this.mediaConstraints).then((sessionDescription) => {
+			console.log("Create offer:" + JSON.stringify(sessionDescription));
+
+			console.log(`video codecs:${Array.from(new Set(RTCRtpReceiver.getCapabilities("video")?.codecs?.map(codec => codec.mimeType)))}`)
+			console.log(`audio codecs:${Array.from(new Set(RTCRtpReceiver.getCapabilities("audio")?.codecs?.map(codec => codec.mimeType)))}`)
+
+			if (prefmime != undefined) {
+				//set prefered codec
+				const [prefkind] = prefmime.split('/');
+				const codecs = RTCRtpReceiver.getCapabilities(prefkind).codecs;
+				const preferedCodecs = codecs.filter(codec => codec.mimeType === prefmime);
+
+				console.log(`preferedCodecs:${JSON.stringify(preferedCodecs)}`);
+				this.pc.getTransceivers().filter(transceiver => transceiver.receiver.track.kind === prefkind).forEach(tcvr => {
+					if(tcvr.setCodecPreferences != undefined) {
+						tcvr.setCodecPreferences(preferedCodecs);
+					}
+				});
+			}
+		
+			
+			this.pc.setLocalDescription(sessionDescription)
+				.then(() => {
+					fetch(callurl, { method: "POST", body: JSON.stringify(sessionDescription) })
+						.then(this._handleHttpErrors)
+						.then( (response) => (response.json()) )
+						.catch( (error) => this.onError("call " + error ))
+						.then( (response) =>  this.onReceiveCall(response) )
+						.catch( (error) => this.onError("call " + error ))
+				
+				}, (error) => {
+					console.log ("setLocalDescription error:" + JSON.stringify(error)); 
+				});
+			
+		}, (error) => { 
+			alert("Create offer error:" + JSON.stringify(error));
+		});
+
+	} catch (e) {
+		this.disconnect();
+		alert("connect error: " + e);
+	}	    
+}
+
+
+WebRtcStreamer.prototype.getIceCandidate = function() {
+	fetch(this.srvurl + "/api/getIceCandidate?peerid=" + this.pc.peerid)
+		.then(this._handleHttpErrors)
+		.then( (response) => (response.json()) )
+		.then( (response) =>  this.onReceiveCandidate(response))
+		.catch( (error) => this.onError("getIceCandidate " + error ))
+}
+					
+/*
+* create RTCPeerConnection 
+*/
+WebRtcStreamer.prototype.createPeerConnection = function() {
+	console.log("createPeerConnection  config: " + JSON.stringify(this.pcConfig));
+	this.pc = new RTCPeerConnection(this.pcConfig);
+	var pc = this.pc;
+	pc.peerid = Math.random();			
+	
+	pc.onicecandidate = (evt) => this.onIceCandidate(evt);
+	pc.onaddstream    = (evt) => this.onAddStream(evt);
+	pc.oniceconnectionstatechange = (evt) => {  
+		console.log("oniceconnectionstatechange  state: " + pc.iceConnectionState);
+		if (this.videoElement) {
+			if (pc.iceConnectionState === "connected") {
+				this.videoElement.style.opacity = "1.0";
+			}			
+			else if (pc.iceConnectionState === "disconnected") {
+				this.videoElement.style.opacity = "0.25";
+			}			
+			else if ( (pc.iceConnectionState === "failed") || (pc.iceConnectionState === "closed") )  {
+				this.videoElement.style.opacity = "0.5";
+			} else if (pc.iceConnectionState === "new") {
+				this.getIceCandidate();
+			}
+		}
+	}
+	pc.ondatachannel = function(evt) {  
+		console.log("remote datachannel created:"+JSON.stringify(evt));
+		
+		evt.channel.onopen = function () {
+			console.log("remote datachannel open");
+			this.send("remote channel openned");
+		}
+		evt.channel.onmessage = function (event) {
+			console.log("remote datachannel recv:"+JSON.stringify(event.data));
+		}
+	}
+
+	try {
+		var dataChannel = pc.createDataChannel("ClientDataChannel");
+		dataChannel.onopen = function() {
+			console.log("local datachannel open");
+			this.send("local channel openned");
+		}
+		dataChannel.onmessage = function(evt) {
+			console.log("local datachannel recv:"+JSON.stringify(evt.data));
+		}
+	} catch (e) {
+		console.log("Cannor create datachannel error: " + e);
+	}	
+	
+	console.log("Created RTCPeerConnnection with config: " + JSON.stringify(this.pcConfig) );
+	return pc;
+}
+
+
+/*
+* RTCPeerConnection IceCandidate callback
+*/
+WebRtcStreamer.prototype.onIceCandidate = function (event) {
+	if (event.candidate) {
+		if (this.pc.currentRemoteDescription)  {
+			this.addIceCandidate(this.pc.peerid, event.candidate);					
+		} else {
+			this.earlyCandidates.push(event.candidate);
+		}
+	} 
+	else {
+		console.log("End of candidates.");
+	}
+}
+
+
+WebRtcStreamer.prototype.addIceCandidate = function(peerid, candidate) {
+	fetch(this.srvurl + "/api/addIceCandidate?peerid="+peerid, { method: "POST", body: JSON.stringify(candidate) })
+		.then(this._handleHttpErrors)
+		.then( (response) => (response.json()) )
+		.then( (response) =>  {console.log("addIceCandidate ok:" + response)})
+		.catch( (error) => this.onError("addIceCandidate " + error ))
+}
+				
+/*
+* RTCPeerConnection AddTrack callback
+*/
+WebRtcStreamer.prototype.onAddStream = function(event) {
+	console.log("Remote track added:" +  JSON.stringify(event));
+	
+	this.videoElement.srcObject = event.stream;
+	var promise = this.videoElement.play();
+	if (promise !== undefined) {
+	  promise.catch((error) => {
+		console.warn("error:"+error);
+		this.videoElement.setAttribute("controls", true);
+	  });
+	}
+}
+		
+/*
+* AJAX /call callback
+*/
+WebRtcStreamer.prototype.onReceiveCall = function(dataJson) {
+
+	console.log("offer: " + JSON.stringify(dataJson));
+	var descr = new RTCSessionDescription(dataJson);
+	this.pc.setRemoteDescription(descr).then(() =>  { 
+			console.log ("setRemoteDescription ok");
+			while (this.earlyCandidates.length) {
+				var candidate = this.earlyCandidates.shift();
+				this.addIceCandidate(this.pc.peerid, candidate);				
+			}
+		
+			this.getIceCandidate()
+		}
+		, (error) => { 
+			console.log ("setRemoteDescription error:" + JSON.stringify(error)); 
+		});
+}	
+
+/*
+* AJAX /getIceCandidate callback
+*/
+WebRtcStreamer.prototype.onReceiveCandidate = function(dataJson) {
+	console.log("candidate: " + JSON.stringify(dataJson));
+	if (dataJson) {
+		for (var i=0; i<dataJson.length; i++) {
+			var candidate = new RTCIceCandidate(dataJson[i]);
+			
+			console.log("Adding ICE candidate :" + JSON.stringify(candidate) );
+			this.pc.addIceCandidate(candidate).then( () =>      { console.log ("addIceCandidate OK"); }
+				, (error) => { console.log ("addIceCandidate error:" + JSON.stringify(error)); } );
+		}
+		this.pc.addIceCandidate();
+	}
+}
+
+
+/*
+* AJAX callback for Error
+*/
+WebRtcStreamer.prototype.onError = function(status) {
+	console.log("onError:" + status);
+}
+
+return WebRtcStreamer;
+})();
+
+if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
+	window.WebRtcStreamer = WebRtcStreamer;
+}
+if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
+	module.exports = WebRtcStreamer;
+}

+ 52 - 0
src/views/camera/index.vue

@@ -0,0 +1,52 @@
+<template>
+  <el-row :gutter="15">
+    <!--安防摄像头接入-->
+    <el-col :span="20" :xs="15">
+      <ContentWrap>
+        <el-input v-model="rtspAddr" class="!w-450px" placeholder="请输入RTSP/RTMP视频流服务地址" />
+        <el-button @click="handleChange">接入</el-button>
+      </ContentWrap>
+      <ContentWrap>
+        <video id="video" autoplay width="500" height="400"></video>
+      </ContentWrap>
+    </el-col>
+  </el-row>
+
+
+</template>
+
+<script>
+let  rtspAddr
+export default {
+  name: 'Webrtc',
+  data() {
+    return {
+      webRtcServer: null,
+    }
+  },
+  mounted() {
+    //转码服务器暂时先写死,然后我在做活,测试用
+    this.webRtcServer = new WebRtcStreamer('video', location.protocol + '//123.60.223.250:18000')
+    //这个拉流的服务暂时写死,为了效果
+    this.rtspAddr = 'rtsp://caoyang:gyee@123.60.223.250:38554/cy12345'
+    this.webRtcServer.connect(this.rtspAddr);
+  },
+  beforeUnmount() {
+    this.webRtcServer.disconnect()
+    this.webRtcServer = null
+  },
+  methods: {
+    handleChange() {
+      console.log(this.rtspAddr);
+      if (this.rtspAddr) {
+        console.log('正在拉流');
+        this.webRtcServer.connect(this.rtspAddr);
+      } else {
+        ElMessage.error('请填写正确地址,地址格式为:rtsp://用户名:密码@地址:端口/应用ID')
+      }
+    }
+  }
+}
+</script>
+
+<style scoped></style>