Meet Media API 개념

Google Meet Media API를 사용하면 앱이 Google Meet 회의에 참여하여 실시간 미디어 스트림을 사용할 수 있습니다.

클라이언트는 WebRTC를 사용하여 Meet 서버와 통신합니다. 제공된 참조 클라이언트 (C++, TypeScript)는 권장사항을 보여주며 이를 기반으로 직접 빌드하는 것이 좋습니다.

하지만 Meet Media API의 기술 요구사항을 준수하는 완전 맞춤 WebRTC 클라이언트를 빌드할 수도 있습니다.

이 페이지에서는 Meet Media API 세션을 성공적으로 진행하는 데 필요한 주요 WebRTC 개념을 간략하게 설명합니다.

Offer-answer 시그널링

WebRTC는 피어가 서로 신호를 보내 통신하는 P2P (피어 투 피어) 프레임워크입니다. 세션을 시작하기 위해 시작 피어가 원격 피어에 SDP 제안을 전송합니다. 이 혜택에는 다음과 같은 중요한 세부정보가 포함됩니다.

오디오 및 동영상의 미디어 설명

미디어 설명은 P2P 세션 중에 전달되는 내용을 나타냅니다. 설명에는 오디오, 동영상, 데이터의 세 가지 유형이 있습니다.

n 오디오 스트림을 나타내기 위해 제안자는 제안에 n 오디오 미디어 설명을 포함합니다. 동영상도 마찬가지입니다. 하지만 데이터 미디어 설명은 최대 하나만 있을 수 있습니다.

통행 방향

각 오디오 또는 동영상 설명은 RFC 3711에 의해 관리되는 개별 보안 실시간 전송 프로토콜 (SRTP) 스트림을 설명합니다. 이는 양방향이므로 두 피어가 동일한 연결을 통해 미디어를 보내고 받을 수 있습니다.

따라서 제안과 대답 모두의 각 미디어 설명에는 스트림 사용 방식을 설명하는 다음 세 가지 속성 중 하나가 포함됩니다.

  • sendonly: 오퍼링 피어의 미디어만 전송합니다. 원격 피어가 이 스트림에서 미디어를 전송하지 않습니다.

  • recvonly: 원격 피어의 미디어만 수신합니다. 제공 피어가 이 스트림에서 미디어를 전송하지 않습니다.

  • sendrecv: 두 피어 모두 이 스트림에서 전송하고 수신할 수 있습니다.

코덱

각 미디어 설명은 피어가 지원하는 코덱도 지정합니다. Meet Media API의 경우 클라이언트가 기술 요구사항에 지정된 코덱을 (최소한) 지원하지 않으면 클라이언트 제안이 거부됩니다.

DTLS 핸드셰이크

SRTP 스트림은 피어 간의 초기 데이터그램 전송 계층 보안 ('DTLS', RFC 9147) 핸드셰이크로 보호됩니다. DTLS는 일반적으로 클라이언트-서버 프로토콜입니다. 신호 프로세스 중에 한 피어는 서버로 작동하는 데 동의하고 다른 피어는 피어로 작동하는 데 동의합니다.

각 SRTP 스트림에는 전용 DTLS 연결이 있을 수 있으므로 각 미디어 설명은 DTLS 핸드셰이크에서 피어의 역할을 나타내는 세 가지 속성 중 하나를 지정합니다.

  • a=setup:actpass: 제공 피어가 원격 피어의 선택을 따릅니다.

  • a=setup:active: 이 피어가 클라이언트 역할을 합니다.

  • a=setup:passive: 이 피어는 서버 역할을 합니다.

애플리케이션 미디어 설명

데이터 채널 (RFC 8831)은 스트림 제어 전송 프로토콜 ('SCTP', RFC 9260)의 추상화입니다.

초기 신호 단계에서 데이터 채널을 열려면 제안에 애플리케이션 미디어 설명이 포함되어야 합니다. 오디오 및 동영상 설명과 달리 애플리케이션 설명은 방향이나 코덱을 지정하지 않습니다.

ICE 후보

피어의 대화형 연결 설정 ('ICE', RFC 8445) 후보는 원격 피어가 연결을 설정하는 데 사용할 수 있는 경로 목록입니다.

두 피어 목록의 데카르트 곱(후보 쌍)은 두 피어 간의 잠재적 경로를 나타냅니다. 이러한 쌍은 최적의 경로를 확인하기 위해 테스트됩니다.

Meet REST API를 통해 신호

Meet REST API를 사용하여 이 offer-answer 시그널링을 실행합니다. 앱은 connectActiveConference() 메서드에 SDP 제안을 제공하고 그 대가로 SDP 응답을 수신합니다.

다음 코드 샘플은 메서드를 호출하는 방법을 보여줍니다.

자바

java-meet/samples/snippets/generated/com/google/apps/meet/v2beta/spacesservice/connectactiveconference/AsyncConnectActiveConference.java
import com.google.api.core.ApiFuture;
import com.google.apps.meet.v2beta.ConnectActiveConferenceRequest;
import com.google.apps.meet.v2beta.ConnectActiveConferenceResponse;
import com.google.apps.meet.v2beta.SpaceName;
import com.google.apps.meet.v2beta.SpacesServiceClient;

public class AsyncConnectActiveConference {

  public static void main(String[] args) throws Exception {
    asyncConnectActiveConference();
  }

  public static void asyncConnectActiveConference() throws Exception {
    // This snippet has been automatically generated and should be regarded as a code template only.
    // It will require modifications to work:
    // - It may require correct/in-range values for request initialization.
    // - It may require specifying regional endpoints when creating the service client as shown in
    // https://cloud.google.com/java/docs/setup#configure_endpoints_for_the_client_library
    try (SpacesServiceClient spacesServiceClient = SpacesServiceClient.create()) {
      ConnectActiveConferenceRequest request =
          ConnectActiveConferenceRequest.newBuilder()
              .setName(SpaceName.of("[SPACE]").toString())
              .setOffer("offer105650780")
              .build();
      ApiFuture<ConnectActiveConferenceResponse> future =
          spacesServiceClient.connectActiveConferenceCallable().futureCall(request);
      // Do something.
      ConnectActiveConferenceResponse response = future.get();
    }
  }
}

C#

apis/Google.Apps.Meet.V2Beta/Google.Apps.Meet.V2Beta.GeneratedSnippets/SpacesServiceClient.ConnectActiveConferenceAsyncSnippet.g.cs
using Google.Apps.Meet.V2Beta;
using System.Threading.Tasks;

public sealed partial class GeneratedSpacesServiceClientSnippets
{
    /// <summary>Snippet for ConnectActiveConferenceAsync</summary>
    /// <remarks>
    /// This snippet has been automatically generated and should be regarded as a code template only.
    /// It will require modifications to work:
    /// - It may require correct/in-range values for request initialization.
    /// - It may require specifying regional endpoints when creating the service client as shown in
    ///   https://cloud.google.com/dotnet/docs/reference/help/client-configuration#endpoint.
    /// </remarks>
    public async Task ConnectActiveConferenceAsync()
    {
        // Create client
        SpacesServiceClient spacesServiceClient = await SpacesServiceClient.CreateAsync();
        // Initialize request argument(s)
        string name = "spaces/[SPACE]";
        // Make the request
        ConnectActiveConferenceResponse response = await spacesServiceClient.ConnectActiveConferenceAsync(name);
    }
}

Node.js

packages/google-apps-meet/samples/generated/v2beta/spaces_service.connect_active_conference.js
/**
 * This snippet has been automatically generated and should be regarded as a code template only.
 * It will require modifications to work.
 * It may require correct/in-range values for request initialization.
 * TODO(developer): Uncomment these variables before running the sample.
 */
/**
 *  Required. Resource name of the space.
 *  Format: spaces/{spaceId}
 */
// const name = 'abc123'
/**
 *  Required. WebRTC SDP (Session Description Protocol) offer from the client.
 *  The format is defined by RFC
 *  8866 (https://www.rfc-editor.org/rfc/rfc8866) with mandatory keys defined
 *  by RFC 8829 (https://www.rfc-editor.org/rfc/rfc8829). This is the standard
 *  SDP format generated by a peer connection's createOffer() and
 *  createAnswer() methods.
 */
// const offer = 'abc123'

// Imports the Meet library
const {SpacesServiceClient} = require('@google-apps/meet').v2beta;

// Instantiates a client
const meetClient = new SpacesServiceClient();

async function callConnectActiveConference() {
  // Construct request
  const request = {
    name,
    offer,
  };

  // Run request
  const response = await meetClient.connectActiveConference(request);
  console.log(response);
}

callConnectActiveConference();

Python

packages/google-apps-meet/samples/generated_samples/meet_v2beta_generated_spaces_service_connect_active_conference_async.py
# This snippet has been automatically generated and should be regarded as a
# code template only.
# It will require modifications to work:
# - It may require correct/in-range values for request initialization.
# - It may require specifying regional endpoints when creating the service
#   client as shown in:
#   https://googleapis.dev/python/google-api-core/latest/client_options.html
from google.apps import meet_v2beta


async def sample_connect_active_conference():
    # Create a client
    client = meet_v2beta.SpacesServiceAsyncClient()

    # Initialize request argument(s)
    request = meet_v2beta.ConnectActiveConferenceRequest(
        name="name_value",
        offer="offer_value",
    )

    # Make the request
    response = await client.connect_active_conference(request=request)

    # Handle the response
    print(response)

연결 흐름 예

오디오 미디어 설명이 포함된 혜택은 다음과 같습니다.

오디오 미디어 설명이 포함된 혜택의 예
그림 1. 오디오 미디어 설명이 포함된 혜택의 예

원격 피어는 동일한 수의 미디어 설명 줄이 포함된 SDP 대답으로 응답합니다. 각 줄은 원격 피어가 SRTP 스트림을 통해 제공 클라이언트에 다시 전송하는 미디어를 나타냅니다. 원격 피어는 미디어 설명 항목을 recvonly로 설정하여 제안자로부터 특정 스트림을 거부할 수도 있습니다.

Meet Media API의 경우 클라이언트는 항상 SDP 오퍼를 전송하여 연결을 시작합니다. Meet은 시작자가 아닙니다.

이 동작은 참조 클라이언트(C++, TypeScript)에 의해 내부적으로 관리되지만 맞춤 클라이언트 개발자는 WebRTC의 PeerConnectionInterface를 사용하여 제안을 생성할 수 있습니다.

Meet에 연결하려면 혜택이 특정 요구사항을 준수해야 합니다.

  1. 클라이언트는 항상 DTLS 핸드셰이크에서 클라이언트 역할을 해야 하므로 제안의 모든 미디어 설명은 a=setup:actpass 또는 a=setup:active를 지정해야 합니다.

  2. 각 미디어 설명 줄은 해당 미디어 유형에 필요한 모든 필수 코덱을 지원해야 합니다.

    • 오디오: Opus
    • 동영상: VP8, VP9, AV1
  3. 오디오를 수신하려면 혜택에 수신 전용 오디오 미디어 설명이 정확히 3개 포함되어야 합니다. 피어 연결 객체에서 트랜시버를 설정하면 됩니다.

    C++

    // ...
    rtc::scoped_refptr<webrtc::PeerConnectionInterface> peer_connection;
    
    for (int i = 0; i < 3; ++i) {
      webrtc::RtpTransceiverInit audio_init;
      audio_init.direction = webrtc::RtpTransceiverDirection::kRecvOnly;
      audio_init.stream_ids = {absl::StrCat("audio_stream_", i)};
    
      webrtc::RTCErrorOr<rtc::scoped_refptr<webrtc::RtpTransceiverInterface>>
        audio_result = peer_connection->AddTransceiver(
          cricket::MediaType::MEDIA_TYPE_AUDIO, audio_init);
    
      if (!audio_result.ok()) {
        return absl::InternalError(absl::StrCat("Failed to add audio transceiver: ",
                                                audio_result.error().message()));
      }
    }
    

    자바스크립트

    pc = new RTCPeerConnection();
    
    // Configure client to receive audio from Meet servers.
    pc.addTransceiver('audio', {'direction':'recvonly'});
    pc.addTransceiver('audio', {'direction':'recvonly'});
    pc.addTransceiver('audio', {'direction':'recvonly'});
    
  4. 동영상을 수신하려면 제품에 수신 전용 동영상 미디어 설명이 1~3개 포함되어야 합니다. 피어 연결 객체에서 트랜시버를 설정하면 됩니다.

    C++

    // ...
    rtc::scoped_refptr<webrtc::PeerConnectionInterface> peer_connection;
    
    for (uint32_t i = 0; i < configurations.receiving_video_stream_count; ++i) {
      webrtc::RtpTransceiverInit video_init;
      video_init.direction = webrtc::RtpTransceiverDirection::kRecvOnly;
      video_init.stream_ids = {absl::StrCat("video_stream_", i)};
    
      webrtc::RTCErrorOr<rtc::scoped_refptr<webrtc::RtpTransceiverInterface>>
          video_result = peer_connection->AddTransceiver(
            cricket::MediaType::MEDIA_TYPE_VIDEO, video_init);
    
      if (!video_result.ok()) {
        return absl::InternalError(absl::StrCat("Failed to add video transceiver: ",
                                                video_result.error().message()));
      }
    }
    

    자바스크립트

    pc = new RTCPeerConnection();
    
    // Configure client to receive video from Meet servers.
    pc.addTransceiver('video', {'direction':'recvonly'});
    pc.addTransceiver('video', {'direction':'recvonly'});
    pc.addTransceiver('video', {'direction':'recvonly'});
    
  5. 혜택에는 항상 데이터 채널이 포함되어야 합니다. 최소한 session-controlmedia-stats 채널은 항상 열려 있어야 합니다. 모든 데이터 채널은 ordered이어야 합니다.

    C++

    // ...
    // All data channels must be ordered.
    constexpr webrtc::DataChannelInit kDataChannelConfig = {.ordered = true};
    
    rtc::scoped_refptr<webrtc::PeerConnectionInterface> peer_connection;
    
    // Signal session-control data channel.
    webrtc::RTCErrorOr<rtc::scoped_refptr<webrtc::DataChannelInterface>>
      session_create_result = peer_connection->CreateDataChannelOrError(
        "session-control", &kDataChannelConfig);
    
    if (!session_create_result.ok()) {
      return absl::InternalError(absl::StrCat("Failed to create data channel ",
                                              data_channel_label, ": ",
                                              session_create_result.error().message()));
    }
    
    // Signal media-stats data channel.
    webrtc::RTCErrorOr<rtc::scoped_refptr<webrtc::DataChannelInterface>>
      stats_create_result = peer_connection->CreateDataChannelOrError(
        "media-stats", &kDataChannelConfig);
    
    if (!stats_create_result.ok()) {
      return absl::InternalError(absl::StrCat("Failed to create data channel ",
                                              data_channel_label, ": ",
                                              stats_create_result.error().message()));
    }
    

    자바스크립트

    // ...
    pc = new RTCPeerConnection();
    
    // All data channels must be ordered.
    const dataChannelConfig = {
      ordered: true,
    };
    
    // Signal session-control data channel.
    sessionControlChannel = pc.createDataChannel('session-control', dataChannelConfig);
    sessionControlChannel.onopen = () => console.log("data channel is now open");
    sessionControlChannel.onclose = () => console.log("data channel is now closed");
    sessionControlChannel.onmessage = async (e) => {
      console.log("data channel message", e.data);
    };
    
    // Signal media-stats data channel.
    mediaStatsChannel = pc.createDataChannel('media-stats', dataChannelConfig);
    mediaStatsChannel.onopen = () => console.log("data channel is now open");
    mediaStatsChannel.onclose = () => console.log("data channel is now closed");
    mediaStatsChannel.onmessage = async (e) => {
      console.log("data channel message", e.data);
    };
    

SDP 제안 및 답변 예시

다음은 유효한 SDP 제안과 일치하는 SDP 답변의 전체 예입니다. 이 제안은 오디오와 단일 동영상 스트림으로 Meet Media API 세션을 협상합니다.

오디오 미디어 설명 3개, 동영상 미디어 설명 1개, 필수 애플리케이션 미디어 설명이 있습니다.

클라이언트 SDP 제안 Meet Media API SDP 응답
v=0
o=- 1479484780199836840 3 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1 2 3 4
a=extmap-allow-mixed
a=msid-semantic: WMS
v=0
o=- 0 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1 2 3 4
a=msid-semantic: WMS virtual-6666 virtual-video-7777/7777
a=ice-lite
m=audio 59905 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126
c=IN IP4 136.55.18.35
a=rtcp:9 IN IP4 0.0.0.0
a=candidate:3490152339 1 udp 2113937151 46ae665f-23fd-49df-a002-6d12bc897a54.local 59905 typ host generation 0 network-cost 999
a=candidate:1937170525 1 udp 2113939711 aa575ae6-68fc-4155-83c1-007a1f5e8e55.local 58304 typ host generation 0 network-cost 999
a=candidate:2999458021 1 udp 1677732095 2605:a601:55ab:b000:615a:2317:bf6b:7a30 58304 typ srflx raddr :: rport 0 generation 0 network-cost 999
a=candidate:2517543359 1 udp 1677729535 136.55.18.35 59905 typ srflx raddr 0.0.0.0 rport 0 generation 0 network-cost 999
a=ice-ufrag:0HPF
a=ice-pwd:GcBv48eO/q64iPxb7MHKS87y
a=ice-options:trickle
a=fingerprint:sha-256 71:0A:DD:DF:D1:63:8E:D5:CB:E6:2B:6D:41:1D:D4:EE:79:B2:95:97:8A:F0:64:FF:10:37:8D:41:ED:DB:EC:C4
a=setup:actpass
a=mid:0
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=recvonly
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:63 red/48000/2
a=fmtp:63 111/111
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:13 CN/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:126 telephone-event/8000
m=audio 19306 UDP/TLS/RTP/SAVPF 111
c=IN IP4 142.250.82.213
a=rtcp:9 IN IP4 0.0.0.0
a=candidate: 1 udp 2113932031 142.250.82.213 19306 typ host generation 0
a=candidate: 1 tcp 2113932030 142.250.82.253 19306 typ host tcptype passive generation 0
a=candidate: 1 ssltcp 2113932029 142.250.82.253 19313 typ host generation 0
a=candidate: 1 udp 2113939711 2001:4860:4864:6:4000::19 19306 typ host generation 0
a=candidate: 1 tcp 2113939710 2001:4860:4864:6:8000::5 19306 typ host tcptype passive generation 0
a=candidate: 1 ssltcp 2113939709 2001:4860:4864:6:8000::5 19313 typ host generation 0
a=ice-ufrag:K8mRD3UolM6pjwoKAhgCCIoBnCgCIAEQ
a=ice-pwd:+7DfqMEDEFB6dLAKfGjT41l7ygg=
a=fingerprint:sha-256 32:C0:9D:17:AD:99:E2:B8:2D:FD:5D:87:D4:36:44:4A:5B:3E:EE:EA:F2:BE:BE:72:3B:66:4C:F2:57:3C:0D:FF
a=setup:passive
a=mid:0
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=sendonly
a=msid:virtual-6666 virtual-6666
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1
a=ssrc:6666 cname:6666
m=audio 9 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:0HPF
a=ice-pwd:GcBv48eO/q64iPxb7MHKS87y
a=ice-options:trickle
a=fingerprint:sha-256 71:0A:DD:DF:D1:63:8E:D5:CB:E6:2B:6D:41:1D:D4:EE:79:B2:95:97:8A:F0:64:FF:10:37:8D:41:ED:DB:EC:C4
a=setup:actpass
a=mid:1
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=recvonly
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:63 red/48000/2
a=fmtp:63 111/111
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:13 CN/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:126 telephone-event/8000
m=audio 9 UDP/TLS/RTP/SAVPF 111
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:K8mRD3UolM6pjwoKAhgCCIoBnCgCIAEQ
a=ice-pwd:+7DfqMEDEFB6dLAKfGjT41l7ygg=
a=fingerprint:sha-256 32:C0:9D:17:AD:99:E2:B8:2D:FD:5D:87:D4:36:44:4A:5B:3E:EE:EA:F2:BE:BE:72:3B:66:4C:F2:57:3C:0D:FF
a=setup:passive
a=mid:1
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=sendonly
a=msid:virtual-6667 virtual-6667
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1
a=ssrc:6667 cname:6667
m=audio 9 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:0HPF
a=ice-pwd:GcBv48eO/q64iPxb7MHKS87y
a=ice-options:trickle
a=fingerprint:sha-256 71:0A:DD:DF:D1:63:8E:D5:CB:E6:2B:6D:41:1D:D4:EE:79:B2:95:97:8A:F0:64:FF:10:37:8D:41:ED:DB:EC:C4
a=setup:actpass
a=mid:2
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=recvonly
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:63 red/48000/2
a=fmtp:63 111/111
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:13 CN/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:126 telephone-event/8000
m=audio 9 UDP/TLS/RTP/SAVPF 111
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:K8mRD3UolM6pjwoKAhgCCIoBnCgCIAEQ
a=ice-pwd:+7DfqMEDEFB6dLAKfGjT41l7ygg=
a=fingerprint:sha-256 32:C0:9D:17:AD:99:E2:B8:2D:FD:5D:87:D4:36:44:4A:5B:3E:EE:EA:F2:BE:BE:72:3B:66:4C:F2:57:3C:0D:FF
a=setup:passive
a=mid:2
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=sendonly
a=msid:virtual-6668 virtual-6668
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1
a=ssrc:6668 cname:6668
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 0.0.0.0
a=ice-ufrag:0HPF
a=ice-pwd:GcBv48eO/q64iPxb7MHKS87y
a=ice-options:trickle
a=fingerprint:sha-256 71:0A:DD:DF:D1:63:8E:D5:CB:E6:2B:6D:41:1D:D4:EE:79:B2:95:97:8A:F0:64:FF:10:37:8D:41:ED:DB:EC:C4
a=setup:actpass
a=mid:3
a=sctp-port:5000
a=max-message-size:262144
m=application 9 DTLS/SCTP 5000
c=IN IP4 0.0.0.0
a=ice-ufrag:K8mRD3UolM6pjwoKAhgCCIoBnCgCIAEQ
a=ice-pwd:+7DfqMEDEFB6dLAKfGjT41l7ygg=
a=fingerprint:sha-256 32:C0:9D:17:AD:99:E2:B8:2D:FD:5D:87:D4:36:44:4A:5B:3E:EE:EA:F2:BE:BE:72:3B:66:4C:F2:57:3C:0D:FF
a=setup:passive
a=mid:3
a=sctpmap:5000 webrtc-datachannel 1024
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 35 36 37 38 102 103 104 105 106 107 108 109 127 125 39 40 41 42 43 44 45 46 47 48 112 113 114 115 116 117 118 49
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:0HPF
a=ice-pwd:GcBv48eO/q64iPxb7MHKS87y
a=ice-options:trickle
a=fingerprint:sha-256 71:0A:DD:DF:D1:63:8E:D5:CB:E6:2B:6D:41:1D:D4:EE:79:B2:95:97:8A:F0:64:FF:10:37:8D:41:ED:DB:EC:C4
a=setup:actpass
a=mid:4
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:13 urn:3gpp:video-orientation
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=recvonly
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtpmap:98 VP9/90000
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 transport-cc
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=fmtp:98 profile-id=0
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtpmap:100 VP9/90000
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 transport-cc
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=fmtp:100 profile-id=2
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtpmap:35 VP9/90000
a=rtcp-fb:35 goog-remb
a=rtcp-fb:35 transport-cc
a=rtcp-fb:35 ccm fir
a=rtcp-fb:35 nack
a=rtcp-fb:35 nack pli
a=fmtp:35 profile-id=1
a=rtpmap:36 rtx/90000
a=fmtp:36 apt=35
a=rtpmap:37 VP9/90000
a=rtcp-fb:37 goog-remb
a=rtcp-fb:37 transport-cc
a=rtcp-fb:37 ccm fir
a=rtcp-fb:37 nack
a=rtcp-fb:37 nack pli
a=fmtp:37 profile-id=3
a=rtpmap:38 rtx/90000
a=fmtp:38 apt=37
a=rtpmap:102 H264/90000
a=rtcp-fb:102 goog-remb
a=rtcp-fb:102 transport-cc
a=rtcp-fb:102 ccm fir
a=rtcp-fb:102 nack
a=rtcp-fb:102 nack pli
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:103 rtx/90000
a=fmtp:103 apt=102
a=rtpmap:104 H264/90000
a=rtcp-fb:104 goog-remb
a=rtcp-fb:104 transport-cc
a=rtcp-fb:104 ccm fir
a=rtcp-fb:104 nack
a=rtcp-fb:104 nack pli
a=fmtp:104 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtpmap:105 rtx/90000
a=fmtp:105 apt=104
a=rtpmap:106 H264/90000
a=rtcp-fb:106 goog-remb
a=rtcp-fb:106 transport-cc
a=rtcp-fb:106 ccm fir
a=rtcp-fb:106 nack
a=rtcp-fb:106 nack pli
a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:107 rtx/90000
a=fmtp:107 apt=106
a=rtpmap:108 H264/90000
a=rtcp-fb:108 goog-remb
a=rtcp-fb:108 transport-cc
a=rtcp-fb:108 ccm fir
a=rtcp-fb:108 nack
a=rtcp-fb:108 nack pli
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtpmap:109 rtx/90000
a=fmtp:109 apt=108
a=rtpmap:127 H264/90000
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f
a=rtpmap:125 rtx/90000
a=fmtp:125 apt=127
a=rtpmap:39 H264/90000
a=rtcp-fb:39 goog-remb
a=rtcp-fb:39 transport-cc
a=rtcp-fb:39 ccm fir
a=rtcp-fb:39 nack
a=rtcp-fb:39 nack pli
a=fmtp:39 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f
a=rtpmap:40 rtx/90000
a=fmtp:40 apt=39
a=rtpmap:41 H264/90000
a=rtcp-fb:41 goog-remb
a=rtcp-fb:41 transport-cc
a=rtcp-fb:41 ccm fir
a=rtcp-fb:41 nack
a=rtcp-fb:41 nack pli
a=fmtp:41 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=f4001f
a=rtpmap:42 rtx/90000
a=fmtp:42 apt=41
a=rtpmap:43 H264/90000
a=rtcp-fb:43 goog-remb
a=rtcp-fb:43 transport-cc
a=rtcp-fb:43 ccm fir
a=rtcp-fb:43 nack
a=rtcp-fb:43 nack pli
a=fmtp:43 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=f4001f
a=rtpmap:44 rtx/90000
a=fmtp:44 apt=43
a=rtpmap:45 AV1/90000
a=rtcp-fb:45 goog-remb
a=rtcp-fb:45 transport-cc
a=rtcp-fb:45 ccm fir
a=rtcp-fb:45 nack
a=rtcp-fb:45 nack pli
a=fmtp:45 level-idx=5;profile=0;tier=0
a=rtpmap:46 rtx/90000
a=fmtp:46 apt=45
a=rtpmap:47 AV1/90000
a=rtcp-fb:47 goog-remb
a=rtcp-fb:47 transport-cc
a=rtcp-fb:47 ccm fir
a=rtcp-fb:47 nack
a=rtcp-fb:47 nack pli
a=fmtp:47 level-idx=5;profile=1;tier=0
a=rtpmap:48 rtx/90000
a=fmtp:48 apt=47
a=rtpmap:112 H264/90000
a=rtcp-fb:112 goog-remb
a=rtcp-fb:112 transport-cc
a=rtcp-fb:112 ccm fir
a=rtcp-fb:112 nack
a=rtcp-fb:112 nack pli
a=fmtp:112 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f
a=rtpmap:113 rtx/90000
a=fmtp:113 apt=112
a=rtpmap:114 H264/90000
a=rtcp-fb:114 goog-remb
a=rtcp-fb:114 transport-cc
a=rtcp-fb:114 ccm fir
a=rtcp-fb:114 nack
a=rtcp-fb:114 nack pli
a=fmtp:114 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=64001f
a=rtpmap:115 rtx/90000
a=fmtp:115 apt=114
a=rtpmap:116 red/90000
a=rtpmap:117 rtx/90000
a=fmtp:117 apt=116
a=rtpmap:118 ulpfec/90000
a=rtpmap:49 flexfec-03/90000
a=rtcp-fb:49 goog-remb
a=rtcp-fb:49 transport-cc
a=fmtp:49 repair-window=10000000
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:K8mRD3UolM6pjwoKAhgCCIoBnCgCIAEQ
a=ice-pwd:+7DfqMEDEFB6dLAKfGjT41l7ygg=
a=fingerprint:sha-256 32:C0:9D:17:AD:99:E2:B8:2D:FD:5D:87:D4:36:44:4A:5B:3E:EE:EA:F2:BE:BE:72:3B:66:4C:F2:57:3C:0D:FF
a=setup:passive
a=mid:4
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:13 urn:3gpp:video-orientation
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=sendonly
a=msid:virtual-video-7777/7777 virtual-video-7777/7777
a=rtcp-mux
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtcp-fb:96 goog-remb
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtpmap:98 VP9/90000
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=rtcp-fb:98 goog-remb
a=fmtp:98 profile-id=0
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=ssrc-group:FID 7777 7778
a=ssrc:7777 cname:7777
a=ssrc:7778 cname:7777