<!doctype html>
<meta charset=utf-8>
<title>Change of msid in remote description should trigger related track events</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
const sdpBase =`v=0
o=- 5511237691691746 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0
a=ice-options:trickle
a=ice-lite
a=msid-semantic:WMS *
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 9 102 0 8 105 13 110 113 126
c=IN IP6 ::
a=rtcp:9 IN IP6 ::
a=rtcp-mux
a=mid:0
a=sendrecv
a=ice-ufrag:z0i8R3C9C4hPRWls
a=ice-pwd:O7bPpOFAqasqoidV4yxnFVbc
a=ice-lite
a=fingerprint:sha-256 B7:9C:0D:C9:D1:42:57:97:82:4D:F9:B7:93:75:49:C3:42:21:5A:DD:9C:B5:ED:53:53:F0:B4:C8:AE:88:7A:E7
a=setup:actpass
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
a=rtpmap:0 PCMU/8000`;

const sdp0 = sdpBase + `
`;

const sdp1 = sdpBase + `
a=msid:1 2
a=ssrc:3 cname:4
a=ssrc:3 msid:1 2
`;

const sdp2 = sdpBase + `
a=ssrc:3 cname:4
a=ssrc:3 msid:1 2
`;

const sdp3 = sdpBase + `
a=msid:1 2
a=ssrc:3 cname:4
a=ssrc:3 msid:3 2
`;

const sdp4 = sdp1.replace('msid-semantic', 'unknownattr');

const sdp5 = sdpBase + `
a=msid:-
`;

const sdp6 = sdpBase + `
a=msid:1 2
a=msid:1 2
`;

async function applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp)
{
    const testTrackPromise = new Promise(resolve  => {
        pc.ontrack = (event) => { resolve([event.track, event.streams]); };
    });
    await pc.setRemoteDescription({type: 'offer', sdp: sdp});
    return testTrackPromise;
}

promise_test(async test => {
    const pc = new RTCPeerConnection();
    test.add_cleanup(() => pc.close());

    const [track, streams] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp0);
    assert_equals(streams.length, 1, "track event has a stream");
}, "When a=msid is absent, the track should still be associated with a stream");

promise_test(async test => {
    const pc = new RTCPeerConnection();
    test.add_cleanup(() => pc.close());

    const [track, streams] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp1);
    assert_equals(streams.length, 1, "track event has a stream");
    assert_equals(streams[0].id, "1", "msid should match");
}, "Source-level msid should be ignored if media-level msid is present");

promise_test(async test => {
    const pc = new RTCPeerConnection();
    test.add_cleanup(() => pc.close());

    const [track, streams] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp2);
    assert_equals(streams.length, 1, "track event has a stream");
    assert_equals(streams[0].id, "1", "msid should match");
}, "Source-level msid should be parsed if media-level msid is absent");

promise_test(async test => {
    const pc = new RTCPeerConnection();
    test.add_cleanup(() => pc.close());

    let track;
    let streams;
    try {
      [track, streams] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp3);
    } catch (e) {
      return;
    }
    assert_equals(streams.length, 1, "track event has a stream");
    assert_equals(streams[0].id, "1", "msid should match");
}, "Source-level msid should be ignored, or an error should be thrown, if a different media-level msid is present");

promise_test(async test => {
    const pc = new RTCPeerConnection();
    test.add_cleanup(() => pc.close());

    const [track, streams] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp4);
    assert_equals(streams.length, 1, "track event has a stream");
    assert_equals(streams[0].id, "1", "msid should match");
}, "stream ids should be found even if msid-semantic is absent");

promise_test(async test => {
    const pc = new RTCPeerConnection();
    test.add_cleanup(() => pc.close());

    const [track, streams] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp5);
    assert_equals(streams.length, 0, "track event has no stream");
}, "a=msid:- should result in a track event with no streams");

promise_test(async test => {
    const pc = new RTCPeerConnection();
    test.add_cleanup(() => pc.close());

    const [track, streams] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp6);
    assert_equals(streams.length, 1, "track event has one stream");
}, "Duplicate a=msid should result in a track event with one stream");

promise_test(async test => {
    const pc = new RTCPeerConnection();
    test.add_cleanup(() => pc.close());

    const [track, streams] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp1);
    assert_equals(streams.length, 1, "track event has a stream");
    assert_equals(streams[0].id, "1", "msid should match");
    const stream = streams[0];

    await pc.setLocalDescription(await pc.createAnswer());

    const testTrackPromise = new Promise((resolve) => { stream.onremovetrack = resolve; });
    await pc.setRemoteDescription({type: 'offer', 'sdp': sdp0});
    await testTrackPromise;

    assert_equals(stream.getAudioTracks().length, 0, "stream should be empty");
}, "Applying a remote description with removed msid should trigger firing a removetrack event on the corresponding stream");

promise_test(async test => {
    const pc = new RTCPeerConnection();
    test.add_cleanup(() => pc.close());

    let [track0, streams0] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp0);

    await pc.setLocalDescription(await pc.createAnswer());

    let [track1, streams1] = await applyRemoteDescriptionAndReturnRemoteTrackAndStreams(pc, sdp1);

    assert_equals(streams1.length, 1, "track event has a stream");
    assert_equals(streams1[0].id, "1", "msid should match");
    assert_equals(streams1[0].getTracks()[0], track0, "track should match");
}, "Applying a remote description with a new msid should trigger firing an event with populated streams");
</script>
