[Pion/WebRTC] 비디오 트랙 활성화 및 비활성화

31210 단어 gotypescriptpion

소개



Microsoft Teams 또는 Zoom과 같은 온라인 회의에 참가할 때 비디오 공유 여부를 선택할 수 있습니다.
이번에는 애플리케이션에 구현해 보겠습니다.




  • webappsample - GitHub

  • MediaStream 수정



    세션 중에 미디어 스트림을 변경하려면 연결을 시작할 때 제안과 응답을 보내 협상해야 합니다.
    미디어 스트림을 변경하려는 쪽은 응답측(클라이언트측)이므로 제안측(서버측)에 세션 업데이트를 요청하는 메시지를 보내는 기능을 추가했습니다.

    sseHub.go




    ...
    func handleReceivedMessage(h *SSEHub, message ClientMessage) {
        switch message.Event {
        case TextEvent:
    ...
        case CandidateEvent:
    ...
        case AnswerEvent:
    ...
        case UpdateEvent:
            // when the offer-side is received this type messages,
            // it will start updating the peer connections.
            signalPeerConnections(h)
        }
    }
    func signalPeerConnections(h *SSEHub) {
        defer func() {
            dispatchKeyFrame(h)
        }()
        for syncAttempt := 0; ; syncAttempt++ {
            if syncAttempt == 25 {
                // Release the lock and attempt a sync in 3 seconds. We might be blocking a RemoveTrack or AddTrack
                go func() {
                    time.Sleep(time.Second * 3)
                    signalPeerConnections(h)
                }()
                return
            }
            if !attemptSync(h) {
                break
            }
        }
    }
    ...
    


    비디오 트랙 활성화/비활성화



    클라이언트측 애플리케이션이 세션 중에 비디오를 공유하지 않는 경우 "getUserMedia"제약 조건이 비디오 사용을 비활성화할 수 있습니다.

    webrtc.controller.ts




    ...
    navigator.mediaDevices.getUserMedia({ video: false, audio: true })
        .then(stream => {
            this.webcamStream = stream;
        });
    ...
    


    처음으로 비디오 트랙 추가



    비디오를 활성화하려면 "getUserMedia"를 다시 실행하고 비디오 트랙을 MediaStream에 추가할 수 있습니다.

    webrtc.controller.ts




    ...
    private addVideoTrack(peerConnection: RTCPeerConnection) {
        navigator.mediaDevices.getUserMedia({ video: true })
            .then(stream => {
                const newVideoTracks = stream.getVideoTracks();
                if (this.webcamStream == null ||
                    newVideoTracks.length <= 0) {
                    return;
                }
                this.localVideo.srcObject = stream;
                this.localVideo.play();
                for (const v of newVideoTracks) {
                    this.webcamStream.addTrack(v);
                    peerConnection.addTrack(v, this.webcamStream);
                }
                if (this.connectionUpdatedEvent != null) {
                    this.connectionUpdatedEvent();
                }
            });
    }
    ...
    


    비디오 제거 및 비디오 다시 추가



    "removeTrack"으로 동영상 공유를 중지할 수 있습니다.
    그러나 로컬 MediaStream 비디오 트랙이 중지되면 다시 추가될 때 원격 비디오 트랙으로 공유되지 않으므로 로컬 MediaStream만 일시 중지됩니다.

    webrtc.controller.ts




    ...
        public switchLocalVideoUsage(used: boolean): void {
            if (this.peerConnection == null ||
                this.webcamStream == null) {
                return;
            }
            const tracks = this.webcamStream.getVideoTracks();
            if (used) {
                if (tracks.length > 0 &&
                    tracks[0] != null) {
                    this.replaceVideoTrack(this.peerConnection, tracks[0]);
                } else {
                    this.addVideoTrack(this.peerConnection);
                }
            } else {
                this.removeVideoTrack(this.peerConnection);
            }
        }
    ...
        /** for re-adding the video */
        private replaceVideoTrack(peerConnection: RTCPeerConnection, track: MediaStreamTrack) {
            this.localVideo.play();
            for (const s of peerConnection.getSenders()) {
                if (s.track == null || s.track.kind === "video") {
                    s.replaceTrack(track);
                }
            }
            for (const t of peerConnection.getTransceivers()) {
                if (t.sender.track?.kind == null ||
                    t.sender.track.kind === "video") {
                    t.direction = "sendrecv";
                }
            }
            if (this.connectionUpdatedEvent != null) {
                this.connectionUpdatedEvent();
            }
        }
        private removeVideoTrack(peerConnection: RTCPeerConnection) {
            const senders = peerConnection.getSenders();
            if (senders.length > 0) {
                this.localVideo.pause();
                for (const s of senders) {
                    if (s.track?.kind === "video") {
                        peerConnection.removeTrack(s);
                    }
                }
                if (this.connectionUpdatedEvent != null) {
                    this.connectionUpdatedEvent();
                }
            }
        }
    ...
    


    재추가 후에도 Transceiver 방향은 "recvonly"에서 변경되지 않고 Answer의 SDP에서 "inactive"로 처리되므로 개별적으로 변경해야 합니다.

    DOM에 MediaStream 추가/제거



    이전에는 수신된 모든 항목에서 "종류"가 "비디오"인 MediaStreamTracks만 DOM 요소로 추가되었습니다.

    이때 비디오 트랙이 없으면 오디오 트랙을 오디오 요소로 추가해야 합니다.

    또한 트랙이 제거된 경우에는 해당 요소를 삭제해야 하지만, 비디오 트랙만 제거된 경우에는 비디오 요소의 "srcObject"에서 오디오 트랙을 가져와 오디오 요소로 다시 추가해야 합니다.

    webrtc.controller.ts




    import * as urlParam from "./urlParamGetter";
    type RemoteTrack = {
        id: string,
        kind: "video"|"audio",
        element: HTMLElement,
    };
    export class MainView {
    ...
        public addRemoteTrack(stream: MediaStream, kind: "video"|"audio", id?: string): void {
            if(this.tracks.some(t => t.id === stream.id)) {
                if(kind === "audio") {
                    return;
                }
                this.removeRemoteTrack(stream.id, "audio");
            }
            const remoteTrack = document.createElement(kind);
            remoteTrack.srcObject = stream;
            remoteTrack.autoplay = true;
            remoteTrack.controls = false;
            this.remoteTrackArea.appendChild(remoteTrack);
            this.tracks.push({
                id: (id == null)? stream.id: id,
                kind,
                element: remoteTrack,
            });        
        }
        public removeRemoteTrack(id: string, kind: "video"|"audio"): void {
            const targets = this.tracks.filter(t => t.id === id);
            if(targets.length <= 0) {
                return;
            }
            if(kind === "video") {
                // the audio tracks must be re-added as audio elements.
                const audioTrack = this.getAudioTrack(targets[0]?.element);
                if(audioTrack != null) {
                    this.addRemoteTrack(new MediaStream([audioTrack]), "audio", id);
                }
            }
            for(const t of targets) {
                this.remoteTrackArea.removeChild(t.element);
            }
            const newTracks = new Array<RemoteTrack>();
            for(const t of this.tracks.filter(t => t.id !== id || (t.id === id && t.kind !== kind))) {
                newTracks.push(t);
            }
            this.tracks = newTracks;
        }
        /** get audio track from "srcObject" of HTMLVideoElements */
        private getAudioTrack(target: HTMLElement|null|undefined): MediaStreamTrack|null {
            if(target == null ||
                !(target instanceof HTMLVideoElement)){
                return null;
            }
            if(target.srcObject == null ||
                !("getAudioTracks" in target.srcObject) ||
                (typeof target.srcObject.getAudioTracks !== "function")) {
                return null;
            }
            const tracks = target.srcObject.getAudioTracks();
            if(tracks.length <= 0 ||
                tracks[0] == null) {
                return null;
            }
            return tracks[0];
        }
    }
    


    자원


  • RFC3264 - An Offer/Answer Model with the Session Description Protocol (SDP)
  • RFC8829 - JavaScript Session Establishment Protocol (JSEP)
  • Media Capture and Streams - W3C
  • WebRTC 1.0: Real-Time Communication Between Browsers - W3C
  • 좋은 웹페이지 즐겨찾기