| <!doctype html> | 
 | <meta charset=utf-8> | 
 | <title>RTCPeerConnection.prototype.removeTrack</title> | 
 | <script src="/resources/testharness.js"></script> | 
 | <script src="/resources/testharnessreport.js"></script> | 
 | <script src="RTCPeerConnection-helper.js"></script> | 
 | <script> | 
 |  'use strict'; | 
 |  | 
 |  // Test is based on the following editor draft: | 
 |  // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html | 
 |  | 
 |  // The following helper functions are called from RTCPeerConnection-helper.js: | 
 |  // generateAnswer | 
 |  // generateMediaStreamTrack | 
 |  | 
 |  /* | 
 |  5.1. RTCPeerConnection Interface Extensions | 
 |  partial interface RTCPeerConnection { | 
 |  ... | 
 |  void removeTrack(RTCRtpSender sender); | 
 |  RTCRtpTransceiver addTransceiver((MediaStreamTrack or DOMString) trackOrKind, | 
 |  optional RTCRtpTransceiverInit init); | 
 |  }; | 
 |  */ | 
 |  | 
 |  // Before calling removeTrack can be tested, one needs to add MediaStreamTracks to | 
 |  // a peer connection. There are two ways for adding MediaStreamTrack: addTrack and | 
 |  // addTransceiver. addTransceiver is a newer API while addTrack has been implemented | 
 |  // in current browsers for some time. As a result some of the removeTrack tests have | 
 |  // two versions so that removeTrack can be partially tested without addTransceiver | 
 |  // and the transceiver APIs being implemented. | 
 |  | 
 |  /* | 
 |  5.1. removeTrack | 
 |  3. If connection's [[isClosed]] slot is true, throw an InvalidStateError. | 
 |  */ | 
 |  test(t => { | 
 |  const pc = new RTCPeerConnection(); | 
 |  const track = generateMediaStreamTrack('audio'); | 
 |  const transceiver = pc.addTransceiver(track); | 
 |  const { sender } = transceiver; | 
 |  | 
 |  pc.close(); | 
 |  assert_throws('InvalidStateError', () => pc.removeTrack(sender)); | 
 |  | 
 |  }, 'addTransceiver - Calling removeTrack when connection is closed should throw InvalidStateError'); | 
 |  | 
 |  promise_test(t => { | 
 |  const pc = new RTCPeerConnection(); | 
 |  | 
 |  return navigator.mediaDevices.getUserMedia({ audio: true }) | 
 |  .then(mediaStream => { | 
 |  const tracks = mediaStream.getTracks(); | 
 |  assert_greater_than(tracks.length, 0, | 
 |  'Expect getUserMedia to return at least one audio track'); | 
 |  | 
 |  const track = tracks[0]; | 
 |  const sender = pc.addTrack(track, mediaStream); | 
 |  | 
 |  pc.close(); | 
 |  assert_throws('InvalidStateError', () => pc.removeTrack(sender)); | 
 |  }); | 
 |  }, 'addTrack - Calling removeTrack when connection is closed should throw InvalidStateError'); | 
 |  | 
 |  test(t => { | 
 |  const pc = new RTCPeerConnection(); | 
 |  const track = generateMediaStreamTrack('audio'); | 
 |  const transceiver = pc.addTransceiver(track); | 
 |  const { sender } = transceiver; | 
 |  | 
 |  const pc2 = new RTCPeerConnection(); | 
 |  pc2.close(); | 
 |  assert_throws('InvalidStateError', () => pc2.removeTrack(sender)); | 
 |  | 
 |  }, 'addTransceiver - Calling removeTrack on different connection that is closed should throw InvalidStateError'); | 
 |  | 
 |  promise_test(t => { | 
 |  const pc = new RTCPeerConnection(); | 
 |  | 
 |  return navigator.mediaDevices.getUserMedia({ audio: true }) | 
 |  .then(mediaStream => { | 
 |  const tracks = mediaStream.getTracks(); | 
 |  assert_greater_than(tracks.length, 0, | 
 |  'Expect getUserMedia to return at least one audio track'); | 
 |  | 
 |  const track = tracks[0]; | 
 |  const sender = pc.addTrack(track, mediaStream); | 
 |  | 
 |  const pc2 = new RTCPeerConnection(); | 
 |  pc2.close(); | 
 |  assert_throws('InvalidStateError', () => pc2.removeTrack(sender)); | 
 |  }); | 
 |  }, 'addTrack - Calling removeTrack on different connection that is closed should throw InvalidStateError'); | 
 |  | 
 |  /* | 
 |  5.1. removeTrack | 
 |  4. If sender was not created by connection, throw an InvalidAccessError. | 
 |  */ | 
 |  test(t => { | 
 |  const pc = new RTCPeerConnection(); | 
 |  const track = generateMediaStreamTrack('audio'); | 
 |  const transceiver = pc.addTransceiver(track); | 
 |  const { sender } = transceiver; | 
 |  | 
 |  const pc2 = new RTCPeerConnection(); | 
 |  assert_throws('InvalidAccessError', () => pc2.removeTrack(sender)); | 
 |  | 
 |  }, 'addTransceiver - Calling removeTrack on different connection should throw InvalidAccessError'); | 
 |  | 
 |  promise_test(t => { | 
 |  const pc = new RTCPeerConnection(); | 
 |  | 
 |  return navigator.mediaDevices.getUserMedia({ audio: true }) | 
 |  .then(mediaStream => { | 
 |  const tracks = mediaStream.getTracks(); | 
 |  assert_greater_than(tracks.length, 0, | 
 |  'Expect getUserMedia to return at least one audio track'); | 
 |  | 
 |  const track = tracks[0]; | 
 |  const sender = pc.addTrack(track, mediaStream); | 
 |  | 
 |  const pc2 = new RTCPeerConnection(); | 
 |  assert_throws('InvalidAccessError', () => pc2.removeTrack(sender)); | 
 |  }); | 
 |  }, 'addTrack - Calling removeTrack on different connection should throw InvalidAccessError') | 
 |  | 
 |  /* | 
 |  5.1. removeTrack | 
 |  7. Set sender.track to null. | 
 |  */ | 
 |  test(t => { | 
 |  const pc = new RTCPeerConnection(); | 
 |  const track = generateMediaStreamTrack('audio'); | 
 |  const transceiver = pc.addTransceiver(track); | 
 |  const { sender } = transceiver; | 
 |  | 
 |  assert_equals(sender.track, track); | 
 |  assert_equals(transceiver.direction, 'sendrecv'); | 
 |  assert_equals(transceiver.currentDirection, null); | 
 |  | 
 |  pc.removeTrack(sender); | 
 |  assert_equals(sender.track, null); | 
 |  assert_equals(transceiver.direction, 'sendrecv', | 
 |  'direction should not be altered'); | 
 |  | 
 |  }, 'addTransceiver - Calling removeTrack with valid sender should set sender.track to null'); | 
 |  | 
 |  promise_test(t => { | 
 |  const pc = new RTCPeerConnection(); | 
 |  | 
 |  return navigator.mediaDevices.getUserMedia({ audio: true }) | 
 |  .then(mediaStream => { | 
 |  const tracks = mediaStream.getTracks(); | 
 |  assert_greater_than(tracks.length, 0, | 
 |  'Expect getUserMedia to return at least one audio track'); | 
 |  | 
 |  const track = tracks[0]; | 
 |  const sender = pc.addTrack(track, mediaStream); | 
 |  | 
 |  assert_equals(sender.track, track); | 
 |  | 
 |  pc.removeTrack(sender); | 
 |  assert_equals(sender.track, null); | 
 |  }); | 
 |  }, 'addTrack - Calling removeTrack with valid sender should set sender.track to null'); | 
 |  | 
 |  /* | 
 |  5.1. removeTrack | 
 |  7. Set sender.track to null. | 
 |  10. If transceiver.currentDirection is sendrecv set transceiver.direction | 
 |  to recvonly. | 
 |  */ | 
 |  promise_test(t => { | 
 |  const pc = new RTCPeerConnection(); | 
 |  const track = generateMediaStreamTrack('audio'); | 
 |  const transceiver = pc.addTransceiver(track); | 
 |  const { sender } = transceiver; | 
 |  | 
 |  assert_equals(sender.track, track); | 
 |  assert_equals(transceiver.direction, 'sendrecv'); | 
 |  assert_equals(transceiver.currentDirection, null); | 
 |  | 
 |  return pc.createOffer() | 
 |  .then(offer => | 
 |  pc.setLocalDescription(offer) | 
 |  .then(() => generateAnswer(offer))) | 
 |  .then(answer => pc.setRemoteDescription(answer)) | 
 |  .then(() => { | 
 |  assert_equals(transceiver.currentDirection, 'sendrecv'); | 
 |  | 
 |  pc.removeTrack(sender); | 
 |  assert_equals(sender.track, null); | 
 |  assert_equals(transceiver.direction, 'recvonly'); | 
 |  assert_equals(transceiver.currentDirection, 'sendrecv', | 
 |  'Expect currentDirection to not change'); | 
 |  }); | 
 |  }, 'Calling removeTrack with currentDirection sendrecv should set direction to recvonly'); | 
 |  | 
 |  /* | 
 |  5.1. removeTrack | 
 |  7. Set sender.track to null. | 
 |  11. If transceiver.currentDirection is sendonly set transceiver.direction | 
 |  to inactive. | 
 |  */ | 
 |  promise_test(t => { | 
 |  const pc = new RTCPeerConnection(); | 
 |  const track = generateMediaStreamTrack('audio'); | 
 |  const transceiver = pc.addTransceiver(track, { direction: 'sendonly' }); | 
 |  const { sender } = transceiver; | 
 |  | 
 |  assert_equals(sender.track, track); | 
 |  assert_equals(transceiver.direction, 'sendonly'); | 
 |  assert_equals(transceiver.currentDirection, null); | 
 |  | 
 |  return pc.createOffer() | 
 |  .then(offer => | 
 |  pc.setLocalDescription(offer) | 
 |  .then(() => generateAnswer(offer))) | 
 |  .then(answer => pc.setRemoteDescription(answer)) | 
 |  .then(() => { | 
 |  assert_equals(transceiver.currentDirection, 'sendonly'); | 
 |  | 
 |  pc.removeTrack(sender); | 
 |  assert_equals(sender.track, null); | 
 |  assert_equals(transceiver.direction, 'inactive'); | 
 |  assert_equals(transceiver.currentDirection, 'sendonly', | 
 |  'Expect currentDirection to not change'); | 
 |  }); | 
 |  }, 'Calling removeTrack with currentDirection sendonly should set direction to inactive'); | 
 |  | 
 |  /* | 
 |  5.1. removeTrack | 
 |  7. Set sender.track to null. | 
 |  9. If transceiver.currentDirection is recvonly or inactive, | 
 |  then abort these steps. | 
 |  */ | 
 |  promise_test(t => { | 
 |  const pc = new RTCPeerConnection(); | 
 |  const track = generateMediaStreamTrack('audio'); | 
 |  const transceiver = pc.addTransceiver(track, { direction: 'recvonly' }); | 
 |  const { sender } = transceiver; | 
 |  | 
 |  assert_equals(sender.track, track); | 
 |  assert_equals(transceiver.direction, 'recvonly'); | 
 |  assert_equals(transceiver.currentDirection, null); | 
 |  | 
 |  return pc.createOffer() | 
 |  .then(offer => | 
 |  pc.setLocalDescription(offer) | 
 |  .then(() => generateAnswer(offer))) | 
 |  .then(answer => pc.setRemoteDescription(answer)) | 
 |  .then(() => { | 
 |  assert_equals(transceiver.currentDirection, 'recvonly'); | 
 |  | 
 |  pc.removeTrack(sender); | 
 |  assert_equals(sender.track, null); | 
 |  assert_equals(transceiver.direction, 'recvonly'); | 
 |  assert_equals(transceiver.currentDirection, 'recvonly'); | 
 |  }); | 
 |  }, 'Calling removeTrack with currentDirection recvonly should not change direction'); | 
 |  | 
 |  /* | 
 |  5.1. removeTrack | 
 |  7. Set sender.track to null. | 
 |  9. If transceiver.currentDirection is recvonly or inactive, | 
 |  then abort these steps. | 
 |  */ | 
 |  promise_test(t => { | 
 |  const pc = new RTCPeerConnection(); | 
 |  const track = generateMediaStreamTrack('audio'); | 
 |  const transceiver = pc.addTransceiver(track, { direction: 'inactive' }); | 
 |  const { sender } = transceiver; | 
 |  | 
 |  assert_equals(sender.track, track); | 
 |  assert_equals(transceiver.direction, 'inactive'); | 
 |  assert_equals(transceiver.currentDirection, null); | 
 |  | 
 |  return pc.createOffer() | 
 |  .then(offer => | 
 |  pc.setLocalDescription(offer) | 
 |  .then(() => generateAnswer(offer))) | 
 |  .then(answer => pc.setRemoteDescription(answer)) | 
 |  .then(() => { | 
 |  assert_equals(transceiver.currentDirection, 'inactive'); | 
 |  | 
 |  pc.removeTrack(sender); | 
 |  assert_equals(sender.track, null); | 
 |  assert_equals(transceiver.direction, 'inactive'); | 
 |  assert_equals(transceiver.currentDirection, 'inactive'); | 
 |  }); | 
 |  }, 'Calling removeTrack with currentDirection inactive should not change direction'); | 
 |  | 
 |  /* | 
 |  TODO | 
 |  5.1. removeTrack | 
 |  Stops sending media from sender. The RTCRtpSender will still appear | 
 |  in getSenders. Doing so will cause future calls to createOffer to | 
 |  mark the media description for the corresponding transceiver as | 
 |  recvonly or inactive, as defined in [JSEP] (section 5.2.2.). | 
 |  | 
 |  When the other peer stops sending a track in this manner, an ended | 
 |  event is fired at the MediaStreamTrack object. | 
 |  | 
 |  6. If sender is not in senders (which indicates that it was removed | 
 |  due to setting an RTCSessionDescription of type "rollback"), | 
 |  then abort these steps. | 
 |  12. Update the negotiation-needed flag for connection. | 
 |  */ | 
 | </script> |