无法在 angular 8 中使用 webRTC 播放远程视频

Unable to play a remote video using webRTC in angular 8

我正在尝试使用 angular 在 chrome 中播放 webRTC 视频。我有 IP 摄像头,我正在使用 Wowza Streaming Engine 将他们的 RTSP 流转换为 webRTC。

Wowza 分享了一个示例 index.html,它使用 webrtc.js github link for these files 来流式传输视频,我可以在其中提及我想要播放的相机流,它正在 chrome.

我在 angular 代码中实现了相同的逻辑。我比较了 Wowza 代码和 angular 代码中的 request/response,它们是相同的,但视频无法播放。我只看到带有视频加载标志的黑屏。

我是 webRTC 的新手,所以不知道应该在哪里调试它。

这是我的 html 代码

<div id="container">

  <div id="buttons">
    <button id="startButton" (click)="start()">Start</button>
  </div>

  <video id="remoteVideo"  autoplay playsinline controls muted style="height:480px;"></video>

  </div>

这里是 component.ts 文件

import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';

const PEER_CONNECTION_CONFIG: RTCConfiguration = {
  iceServers: []
};

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  private peerConnection: RTCPeerConnection;
  private signalingConnection: WebSocket;
  userData = { param1: "value1" };
  streamInfo = { applicationName: "webrtc", streamName: "1.stream", sessionId: "[empty]" };

  constructor(fb: FormBuilder) {
  }

  ngOnInit(): void {
  }

  start() {
    this.setupSignalingServer();
  }

  private setupSignalingServer() {
    const self = this;
    this.signalingConnection = new WebSocket(`wss://******.com/webrtc-session.json`);
    this.signalingConnection.binaryType = 'arraybuffer';
    this.signalingConnection.onopen = function (res) {
      console.log('connection open');
      self.setupPeerServer();
      self.signalingConnection.onmessage = self.getSignalMessageCallback();
      self.signalingConnection.onerror = self.errorHandler;

    };
    this.signalingConnection.onclose = function (r) {
      console.log('close');
    };
  }

  private setupPeerServer() {
    const self = this;
    this.peerConnection = new RTCPeerConnection(PEER_CONNECTION_CONFIG);
    this.peerConnection.onicecandidate = this.getIceCandidateCallback();
    this.peerConnection.ontrack = this.gotRemoteTrack;
    console.log("sendPlayGetOffer: " + JSON.stringify(self.streamInfo));
    self.signalingConnection.send('{"direction":"play", "command":"getOffer", "streamInfo":' +
      JSON.stringify(self.streamInfo) + ', "userData":' + JSON.stringify(self.userData) + '}');
  }

  gotRemoteTrack(event) {
    console.log(event);
    console.log('gotRemoteTrack: kind:' + event.track.kind + ' stream:' + event.streams[0]);
    const remoteVideo = document.querySelector('video');
    try {
      remoteVideo.srcObject = event.streams[0];
    } catch (error) {
      console.log(error);
    }
  }
  private getSignalMessageCallback(): (string) => void {
    return (message) => {
      console.log("wsConnection.onmessage: " + message.data);
      const signal = JSON.parse(message.data);
      const streamInfoResponse = signal['streamInfo'];
      if (streamInfoResponse !== undefined) {
        this.streamInfo.sessionId = streamInfoResponse.sessionId;
      }

      console.log('Received signal');
      console.log(signal);
      const msgCommand = signal['command'];

      if (signal.sdp) {
        console.log('sdp: ' + JSON.stringify(signal['sdp']));
        this.peerConnection.setRemoteDescription(new RTCSessionDescription(signal.sdp))
          .then(() => {
            if (signal.sdp) {
              this.peerConnection.createAnswer()
                .then(this.setDescription())
                .catch(this.errorHandler);
            }
          })
          .catch(this.errorHandler);
      } else if (signal.ice) {
        console.log('ice: ' + JSON.stringify(signal.ice));
        this.peerConnection.addIceCandidate(new RTCIceCandidate(signal.ice)).catch(this.errorHandler);
      }
      if ('sendResponse'.localeCompare(msgCommand) == 0) {
        if (this.signalingConnection != null) {
          this.signalingConnection.close();
        }
        this.signalingConnection = null;
      }
    };
  }

  private getIceCandidateCallback(): (string) => void {
    return (event) => {
      console.log(`got ice candidate:`);
      console.log(event);

      if (event.candidate != null) {
      }
    };
  }

  private setDescription(): (string) => void {
    return (description) => {
      console.log('got description ');
      console.log(description);

      this.peerConnection.setLocalDescription(description)
        .then(() => {
          console.log('sendAnswer');
          this.signalingConnection.send('{"direction":"play", "command":"sendResponse", "streamInfo":' +
            JSON.stringify(this.streamInfo) + ', "sdp":' + JSON.stringify(description) + ',"userData":' + JSON.stringify(this.userData) + '}');
        })
        .catch(this.errorHandler);
    };
  }

  private errorHandler(error) {
    console.log(error);
  }
}

我比较了 Wowza 代码和 angular 代码的控制台日志,它似乎与我相同。

Wowza 示例控制台日志

webrtc.js:218 startPlay: wsURL:wss://******.com/webrtc-session.json streamInfo:{"applicationName":"webrtc","streamName":"1.stream","sessionId":"[empty]"}
webrtc.js:68 websockerURL: wss://*******/webrtc-session.json
webrtc.js:73 wsConnection.onopen
webrtc.js:77 peerConnectionConfig
webrtc.js:89 wsURL: wss://*******.com/webrtc-session.json
webrtc.js:103 sendPlayGetOffer: {"applicationName":"webrtc","streamName":"1.stream","sessionId":"[empty]"}
webrtc.js:117 wsConnection.onmessage: {"status":200,"statusDescription":"OK","direction":"play","command":"getOffer","streamInfo":{"applicationName":"webrtc/_definst_","streamName":"1.stream","sessionId":"371518302"},"sdp":{"type":"offer","sdp":"v=0\r\no=WowzaStreamingEngine-next 1888398353 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=fingerprint:sha-256 D9:D4:58:EF:E6:F7:B4:A2:93:C1:2A:FA:FB:FD:B1:EB:65:10:79:D5:E5:6A:BB:89:E5:6C:6E:F9:AB:56:54:67\r\na=group:BUNDLE video\r\na=ice-options:trickle\r\na=msid-semantic:WMS *\r\nm=video 9 RTP/SAVPF 97\r\na=rtpmap:97 H264/90000\r\na=fmtp:97 packetization-mode=1;profile-level-id=42C01E;sprop-parameter-sets=Z0LAHtkDxWhAAAADAEAAAAwDxYuS,aMuMsg==\r\na=cliprect:0,0,160,240\r\na=framesize:97 240-160\r\na=framerate:24.0\r\na=control:trackID=1\r\nc=IN IP4 0.0.0.0\r\na=sendrecv\r\na=ice-pwd:58e0f39dedf7e3d6096a6b90ebe72155\r\na=ice-ufrag:46613e36\r\na=mid:video\r\na=msid:{31104203-d432-4bca-a4ce-e478a708a162} {6ef45b35-493d-4075-8029-747aae04e340}\r\na=rtcp-fb:97 nack\r\na=rtcp-fb:97 nack pli\r\na=rtcp-fb:97 ccm fir\r\na=rtcp-mux\r\na=setup:actpass\r\na=ssrc:457240419 cname:{e3dff516-6bf1-475b-bca9-5f156fa39990}\r\n"}}
webrtc.js:155 sdp: {"type":"offer","sdp":"v=0\r\no=WowzaStreamingEngine-next 1888398353 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=fingerprint:sha-256 D9:D4:58:EF:E6:F7:B4:A2:93:C1:2A:FA:FB:FD:B1:EB:65:10:79:D5:E5:6A:BB:89:E5:6C:6E:F9:AB:56:54:67\r\na=group:BUNDLE video\r\na=ice-options:trickle\r\na=msid-semantic:WMS *\r\nm=video 9 RTP/SAVPF 97\r\na=rtpmap:97 H264/90000\r\na=fmtp:97 packetization-mode=1;profile-level-id=42C01E;sprop-parameter-sets=Z0LAHtkDxWhAAAADAEAAAAwDxYuS,aMuMsg==\r\na=cliprect:0,0,160,240\r\na=framesize:97 240-160\r\na=framerate:24.0\r\na=control:trackID=1\r\nc=IN IP4 0.0.0.0\r\na=sendrecv\r\na=ice-pwd:58e0f39dedf7e3d6096a6b90ebe72155\r\na=ice-ufrag:46613e36\r\na=mid:video\r\na=msid:{31104203-d432-4bca-a4ce-e478a708a162} {6ef45b35-493d-4075-8029-747aae04e340}\r\na=rtcp-fb:97 nack\r\na=rtcp-fb:97 nack pli\r\na=rtcp-fb:97 ccm fir\r\na=rtcp-mux\r\na=setup:actpass\r\na=ssrc:457240419 cname:{e3dff516-6bf1-475b-bca9-5f156fa39990}\r\n"}
webrtc.js:309 RTCTrackEvent {isTrusted: true, receiver: RTCRtpReceiver, track: MediaStreamTrack, streams: Array(1), transceiver: RTCRtpTransceiver, …}
webrtc.js:311 gotRemoteTrack: kind:video stream:[object MediaStream]
webrtc.js:297 gotDescription
webrtc.js:300 sendAnswer
webrtc.js:287 got ice candidate
webrtc.js:288 RTCPeerConnectionIceEvent {isTrusted: true, candidate: RTCIceCandidate, type: "icecandidate", target: RTCPeerConnection, currentTarget: RTCPeerConnection, …}
webrtc.js:117 wsConnection.onmessage: {"status":200,"statusDescription":"OK","direction":"play","command":"sendResponse","streamInfo":{"applicationName":"webrtc/_definst_","streamName":"1.stream","sessionId":"371518302"},"iceCandidates":[{"candidate":"candidate:0 1 TCP 50 172.30.6.139 1935 typ host generation 0","sdpMid":"","sdpMLineIndex":0}]}
webrtc.js:167 iceCandidates: {"candidate":"candidate:0 1 TCP 50 172.30.6.139 1935 typ host generation 0","sdpMid":"","sdpMLineIndex":0}
webrtc.js:189 wsConnection.onclose
webrtc.js:287 got ice candidate
webrtc.js:288 RTCPeerConnectionIceEvent {isTrusted: true, candidate: null, type: "icecandidate", target: RTCPeerConnection, currentTarget: RTCPeerConnection, …}

Angular 代码控制台日志

app.component.ts:34 connection open
app.component.ts:50 sendPlayGetOffer: {"applicationName":"webrtc","streamName":"1.stream","sessionId":"[empty]"}
app.component.ts:67 wsConnection.onmessage: {"status":200,"statusDescription":"OK","direction":"play","command":"getOffer","streamInfo":{"applicationName":"webrtc/_definst_","streamName":"1.stream","sessionId":"500786050"},"sdp":{"type":"offer","sdp":"v=0\r\no=WowzaStreamingEngine-next 1006993747 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=fingerprint:sha-256 D9:D4:58:EF:E6:F7:B4:A2:93:C1:2A:FA:FB:FD:B1:EB:65:10:79:D5:E5:6A:BB:89:E5:6C:6E:F9:AB:56:54:67\r\na=group:BUNDLE video\r\na=ice-options:trickle\r\na=msid-semantic:WMS *\r\nm=video 9 RTP/SAVPF 97\r\na=rtpmap:97 H264/90000\r\na=fmtp:97 packetization-mode=1;profile-level-id=42C01E;sprop-parameter-sets=Z0LAHtkDxWhAAAADAEAAAAwDxYuS,aMuMsg==\r\na=cliprect:0,0,160,240\r\na=framesize:97 240-160\r\na=framerate:24.0\r\na=control:trackID=1\r\nc=IN IP4 0.0.0.0\r\na=sendrecv\r\na=ice-pwd:30ac7ef1eaa28bfd4e7d7c3f20b6e2b2\r\na=ice-ufrag:f6a6a6f7\r\na=mid:video\r\na=msid:{24f3d923-96ef-4286-b232-5ef77b793d19} {ed46e803-398b-4065-b2c9-f92d142bd9a9}\r\na=rtcp-fb:97 nack\r\na=rtcp-fb:97 nack pli\r\na=rtcp-fb:97 ccm fir\r\na=rtcp-mux\r\na=setup:actpass\r\na=ssrc:1674869182 cname:{9ceeb6e4-2745-4052-9c42-127092b3ff74}\r\n"}}
app.component.ts:74 Received signal
app.component.ts:75 {status: 200, statusDescription: "OK", direction: "play", command: "getOffer", streamInfo: {…}, …}
app.component.ts:79 sdp: {"type":"offer","sdp":"v=0\r\no=WowzaStreamingEngine-next 1006993747 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=fingerprint:sha-256 D9:D4:58:EF:E6:F7:B4:A2:93:C1:2A:FA:FB:FD:B1:EB:65:10:79:D5:E5:6A:BB:89:E5:6C:6E:F9:AB:56:54:67\r\na=group:BUNDLE video\r\na=ice-options:trickle\r\na=msid-semantic:WMS *\r\nm=video 9 RTP/SAVPF 97\r\na=rtpmap:97 H264/90000\r\na=fmtp:97 packetization-mode=1;profile-level-id=42C01E;sprop-parameter-sets=Z0LAHtkDxWhAAAADAEAAAAwDxYuS,aMuMsg==\r\na=cliprect:0,0,160,240\r\na=framesize:97 240-160\r\na=framerate:24.0\r\na=control:trackID=1\r\nc=IN IP4 0.0.0.0\r\na=sendrecv\r\na=ice-pwd:30ac7ef1eaa28bfd4e7d7c3f20b6e2b2\r\na=ice-ufrag:f6a6a6f7\r\na=mid:video\r\na=msid:{24f3d923-96ef-4286-b232-5ef77b793d19} {ed46e803-398b-4065-b2c9-f92d142bd9a9}\r\na=rtcp-fb:97 nack\r\na=rtcp-fb:97 nack pli\r\na=rtcp-fb:97 ccm fir\r\na=rtcp-mux\r\na=setup:actpass\r\na=ssrc:1674869182 cname:{9ceeb6e4-2745-4052-9c42-127092b3ff74}\r\n"}
app.component.ts:56 RTCTrackEvent {isTrusted: true, receiver: RTCRtpReceiver, track: MediaStreamTrack, streams: Array(1), transceiver: RTCRtpTransceiver, …}
app.component.ts:57 gotRemoteTrack: kind:video stream:[object MediaStream]
app.component.ts:114 got description 
app.component.ts:115 RTCSessionDescription {type: "answer", sdp: "v=0
↵o=- 554753582248755363 2 IN IP4 127.0.0.1
↵s=…=1;packetization-mode=1;profile-level-id=42e01e
↵"}
app.component.ts:119 sendAnswer
app.component.ts:104 got ice candidate:
app.component.ts:105 RTCPeerConnectionIceEvent {isTrusted: true, candidate: RTCIceCandidate, type: "icecandidate", target: RTCPeerConnection, currentTarget: RTCPeerConnection, …}
app.component.ts:67 wsConnection.onmessage: {"status":200,"statusDescription":"OK","direction":"play","command":"sendResponse","streamInfo":{"applicationName":"webrtc/_definst_","streamName":"1.stream","sessionId":"500786050"},"iceCandidates":[{"candidate":"candidate:0 1 TCP 50 172.30.6.139 1935 typ host generation 0","sdpMid":"","sdpMLineIndex":0}]}
app.component.ts:74 Received signal
app.component.ts:75 {status: 200, statusDescription: "OK", direction: "play", command: "sendResponse", streamInfo: {…}, …}
app.component.ts:41 close
app.component.ts:104 got ice candidate:
app.component.ts:105 RTCPeerConnectionIceEvent {isTrusted: true, candidate: null, type: "icecandidate", target: RTCPeerConnection, currentTarget: RTCPeerConnection, …}

任何人都可以就如何调试提出一些建议吗?

here 了解 IceCandidate 的概念后,我已经解决了我的问题。 我没有在 sdp 报价中正确检查 IceCandidate。 我改变了

else if (signal.ice) {
        console.log('ice: ' + JSON.stringify(signal.ice));
        this.peerConnection.addIceCandidate(new RTCIceCandidate(signal.ice)).catch(this.errorHandler);
      }
else if (signal.iceCandidates) {
        console.log('ice: ' + JSON.stringify(signal.iceCandidates));
        this.peerConnection.addIceCandidate(new RTCIceCandidate(signal.iceCandidates[0])).catch(this.errorHandler);
      }

现在视频在 chrome 浏览器中播放完美。