### Direct Livestreaming Example Source: https://github.com/pili-engineering/qnrtc-web/blob/master/API-Examples-Web/directLivestreaming/index.html This snippet shows how to create a client, join a room, publish local camera and microphone tracks, and start a direct live stream. It also includes event listeners for stream state changes and handles stream stopping and cleanup. ```javascript const client = QNRTC.createClient(); const startBtn = document.getElementById("start-stream-btn"); const stopBtn = document.getElementById("stop-stream-btn"); const roomTokenInput = document.getElementById("room-token"); const streamURLInput = document.getElementById("stream-url"); let localTracks = []; let streamID; // 监控推流状态 function handleStreamStateChanged(streamID, state) { if (state === "STARTED") { alert("streaming state: " + state, "success"); } else { alert("streaming state: " + state, "warning"); } } startBtn.onclick = async (e) => { e.preventDefault(); try { client.on("direct-livestreaming-state-changed", handleStreamStateChanged); await client.join(roomTokenInput.value); const cameraTrack = await QNRTC.createCameraVideoTrack(); const microphoneTrack = await QNRTC.createMicrophoneAudioTrack(); localTracks.push(cameraTrack, microphoneTrack); await client.publish(localTracks); streamID = String(Math.random()), await client.startDirectLiveStreaming({ streamID: streamID, url: streamURLInput.value, videoTrack: cameraTrack, audioTrack: microphoneTrack, }); startBtn.disabled = true; stopBtn.disabled = false; } catch (e) { alert(e.message, "warning"); } }; stopBtn.onclick = (e) => { e.preventDefault(); client.stopDirectLiveStreaming(streamID) .then(() => { return client.leave(); }) .then(() => { for (const track of localTracks) { track.destroy(); } localTracks = []; startBtn.disabled = false; stopBtn.disabled = true; }) .catch((e) => { alert(e.message, "warning"); }); } ``` -------------------------------- ### Initialize Client and UI Elements Source: https://github.com/pili-engineering/qnrtc-web/blob/master/API-Examples-Web/muteAudioOrVideo/index.html Initializes the QNRTC client and gets references to various UI elements for room control and media display. This setup is necessary before joining a room. ```javascript const client = QNRTC.createClient(); const joinRoomBtn = document.getElementById("join-room-btn"); const leaveRoomBtn = document.getElementById("leave-room-btn"); const roomTokenInput = document.getElementById("room-token"); const mediaContainer = document.getElementById("media-container"); const muteControlContainer = document.getElementById("mute-control-container"); const muteCameraInput = document.getElementById("mute-camera"); const muteMicrophoneInput = document.getElementById("mute-microphone"); let microphoneTrack; let cameraTrack; ``` -------------------------------- ### Start Livestreaming Source: https://github.com/pili-engineering/qnrtc-web/blob/master/API-Examples-Web/defaultTranscodingLivestreaming/index.html Handles the start stream button click event. It joins the room, creates and publishes local camera and microphone tracks, and updates the transcoding layout. ```javascript startBtn.onclick = async (e) => { e.preventDefault(); try { client.on("user-published", handleUserPublished); client.on("user-unpublished", handleUserUnPublished); await client.join(roomTokenInput.value); const cameraTrack = await QNRTC.createCameraVideoTrack(); const microphoneTrack = await QNRTC.createMicrophoneAudioTrack(); localTracks.push(cameraTrack, microphoneTrack); await client.publish(localTracks); allVideoTracks.add(cameraTrack.trackID); allAudioTracks.add(microphoneTrack.trackID); setTranscodingTracks(); stopBtn.disabled = false; startBtn.disabled = true; } catch (e) { alert(e.message, "warning"); } }; ``` -------------------------------- ### Initialize Client and Handle UI Events Source: https://github.com/pili-engineering/qnrtc-web/blob/master/API-Examples-Web/defaultTranscodingLivestreaming/index.html Initializes the QNRTC client and sets up event listeners for UI elements like start and stop buttons. It also includes logic for handling user publishing and unpublishing events to update the transcoding layout. ```javascript const client = QNRTC.createClient(); const startBtn = document.getElementById("start-stream-btn"); const stopBtn = document.getElementById("stop-stream-btn"); const roomTokenInput = document.getElementById("room-token"); let localTracks = []; // 合流后画布宽高 // 这里要和浏览器控制台配置的连麦合流转推配置一致 const width = 720; const height = 1080; // 房间内所有的 tracks let allVideoTracks = new Set(); let allAudioTracks = new Set(); // x, y, width, height // 根据 allVideoTracks 自动配置合流布局 // 按照均等分配的原则分配每个 track 在画布的位置 // 1 => 占据全部 // 2 => 左右等分 // 3 => 四等分 // 4 => 四等分 // 5 => 2 row 3 colum // ... function getVideoConfig() { let x = 1; let y = 1; let flag = true; while (x * y < allVideoTracks.size) { if (flag) { x += 1; } if (!flag) { y += 1; } flag = !flag; } const cellWidth = Math.floor(width / x); const cellHeight = Math.floor(height / y); let res = []; let tracks = [...allVideoTracks]; for (let i = 0; i < tracks.length; i++) { let config = {}; config.x = cellWidth * (i % x); config.y = cellHeight * Math.floor(i / x); config.width = (i + 1) % x === 0 ? width - config.x : cellWidth; config.height = Math.floor(i / x) + 1 === y ? height - config.y : cellHeight; config.trackID = tracks[i]; res.push(config); } return res; } // 根据当前房间内所有的 track 信息,更新推流布局 function setTranscodingTracks() { let transcodingTracks = [...[...allAudioTracks].map(trackID => ({ trackID })), ...getVideoConfig()]; // 默认合流,不需要传递 streamID client.setTranscodingLiveStreamingTracks(null, transcodingTracks) .then(() => { console.log("set track success", transcodingTracks); }) .catch(e => { console.log("set track fail", e, transcodingTracks); }); } function handleUserPublished(userID, tracks) { for (const track of tracks) { if (track.isAudio()) { allAudioTracks.add(track.trackID); } else { allVideoTracks.add(track.trackID); } } setTranscodingTracks(); } function handleUserUnPublished(userID, tracks) { for (const track of tracks) { if (track.isAudio()) { allAudioTracks.delete(track.trackID); } else { allVideoTracks.delete(track.trackID); } } setTranscodingTracks(); } ``` -------------------------------- ### Initialize and Get Media Devices Source: https://github.com/pili-engineering/qnrtc-web/blob/master/API-Examples-Web/switchMediaDevices/index.html Initializes the QNRTC client and retrieves lists of available cameras, microphones, and playback devices. Updates the respective select elements with these devices. This should be called on page load. ```javascript const client = QNRTC.createClient(); const joinRoomBtn = document.getElementById("join-room-btn"); const leaveRoomBtn = document.getElementById("leave-room-btn"); const roomTokenInput = document.getElementById("room-token"); const mediaContainer = document.getElementById("media-container"); const cameraSelect = document.getElementById("camera-select"); const microphoneSelect = document.getElementById("microphone-select"); const playbackDeviceSelect = document.getElementById("playbackDevice-select"); let cameras = []; let microphones = []; let playbackDevices = []; function createSelectOption(label, deviceId) { const option = document.createElement("option"); option.value = deviceId; option.innerText = label; return option; } function updateDeviceSelect(devices, selectElement) { selectElement.innerHTML = ""; selectElement.appendChild(createSelectOption("-", "-")); for (const device of devices) { selectElement.appendChild(createSelectOption(device.label, device.deviceId)); } } function getCameras() { QNRTC.getCameras() .then(devices => { cameras = devices; updateDeviceSelect(devices, cameraSelect); }) .catch(e => { alert(e.message, "error"); }); } function getMicrophones() { QNRTC.getMicrophones() .then(devices => { microphones = devices; updateDeviceSelect(devices, microphoneSelect); }) .catch(e => { alert(e.message, "error"); }); } function getPlaybackDevices() { QNRTC.getPlaybackDevices() .then(devices => { playbackDevices = devices; updateDeviceSelect(devices, playbackDeviceSelect); }) .catch(e => { alert(e.message, "error"); }); } getCameras(); getMicrophones(); getPlaybackDevices(); ``` -------------------------------- ### Start Transcoding Livestream Source: https://github.com/pili-engineering/qnrtc-web/blob/master/API-Examples-Web/transcodingLivestreaming/index.html Initiates a transcoded livestream. It joins a room, publishes local tracks, and starts the transcoding process. Ensure you have a valid room token and RTMP stream URL. ```javascript const client = QNRTC.createClient(); const startBtn = document.getElementById("start-stream-btn"); const stopBtn = document.getElementById("stop-stream-btn"); const roomTokenInput = document.getElementById("room-token"); const streamURLInput = document.getElementById("stream-url"); let localTracks = []; let streamID; // 合流后画布宽高 const width = 1280; const height = 720; // 房间内所有的 tracks let allVideoTracks = new Set(); let allAudioTracks = new Set(); // 监控推流状态 function handleStreamStateChanged(streamID, state) { if (state === "STARTED") { setTranscodingTracks(); alert("streaming state: " + state, "success"); } else { alert("streaming state: " + state, "warning"); } } // x, y, width, height // 根据 allVideoTracks 自动配置合流布局 // 按照均等分配的原则分配每个 track 在画布的位置 // 1 => 占据全部 // 2 => 左右等分 // 3 => 四等分 // 4 => 四等分 // 5 => 2 row 3 colum // ... function getVideoConfig() { let x = 1; let y = 1; let flag = true; while (x * y < allVideoTracks.size) { if (flag) { x += 1; } if (!flag) { y += 1; } flag = !flag; } const cellWidth = Math.floor(width / x); const cellHeight = Math.floor(height / y); let res = []; let tracks = [...allVideoTracks]; for (let i = 0; i < tracks.length; i++) { let config = {}; config.x = cellWidth * (i % x); config.y = cellHeight * Math.floor(i / x); config.width = (i + 1) % x === 0 ? width - config.x : cellWidth; config.height = Math.floor(i / x) + 1 === y ? height - config.y : cellHeight; config.trackID = tracks[i]; res.push(config); } return res; } // 根据当前房间内所有的 track 信息,更新推流布局 function setTranscodingTracks() { let transcodingTracks = [...[...allAudioTracks].map(trackID => ({ trackID })), ...getVideoConfig()]; client.setTranscodingLiveStreamingTracks(streamID, transcodingTracks) .then(() => { console.log("set track success", transcodingTracks); }) .catch(e => { console.log("set track fail", e, transcodingTracks); }); } function handleUserPublished(userID, tracks) { for (const track of tracks) { if (track.isAudio()) { allAudioTracks.add(track.trackID); } else { allVideoTracks.add(track.trackID); } } setTranscodingTracks(); } function handleUserUnPublished(userID, tracks) { for (const track of tracks) { if (track.isAudio()) { allAudioTracks.delete(track.trackID); } else { allVideoTracks.delete(track.trackID); } } setTranscodingTracks(); } startBtn.onclick = async (e) => { e.preventDefault(); try { client.on("transcoding-livestreaming-state-changed", handleStreamStateChanged); client.on("user-published", handleUserPublished); client.on("user-unpublished", handleUserUnPublished); await client.join(roomTokenInput.value); const cameraTrack = await QNRTC.createCameraVideoTrack(); const microphoneTrack = await QNRTC.createMicrophoneAudioTrack(); localTracks.push(cameraTrack, microphoneTrack); await client.publish(localTracks); allVideoTracks.add(cameraTrack.trackID); allAudioTracks.add(microphoneTrack.trackID); streamID = String(Math.random()); await client.startTranscodingLiveStreaming({ streamID: streamID, url: streamURLInput.value, width, height }); startBtn.disabled = true; stopBtn.disabled = false; } catch (e) { alert(e.message, "warning"); } }; ``` -------------------------------- ### Initialize and Join Room with External Audio Source: https://github.com/pili-engineering/qnrtc-web/blob/master/API-Examples-Web/publishExternalAudio/index.html This snippet shows the complete process of initializing the QNRTC client, setting up event listeners for user publications, selecting an audio file, creating a buffer source audio track from the selected file, publishing it, and joining the room. It also includes logic for disabling/enabling join/leave buttons and handling potential errors. ```javascript const client = QNRTC.createClient(); const joinRoomBtn = document.getElementById("join-room-btn"); const leaveRoomBtn = document.getElementById("leave-room-btn"); const selectAudioBtn = document.getElementById("select-audio-btn"); const selectAudioInput = document.getElementById("select-audio-input"); const audioFileName = document.getElementById("audio-file-name"); const roomTokenInput = document.getElementById("room-token"); const mediaContainer = document.getElementById("media-container"); let audioFile; selectAudioBtn.onclick = e => { e.preventDefault(); selectAudioInput.click(); }; selectAudioInput.onchange = e => { audioFile = e.target.files[0]; if (!audioFile) return; joinRoomBtn.disabled = false; audioFileName.innerText = audioFile.name; }; let localTracks = []; async function handleUserPublished(userID, tracks) { try { const { videoTracks, audioTracks } = await client.subscribe(tracks); playUserTracks([...videoTracks, ...audioTracks]); } catch (e) { alert(e.message, "warning"); } } function playUserTracks(tracks) { for (const track of tracks) { if (track.isAudio()) { track.play(document.body) .catch(e => { alert(e.message, "warning"); }); } else { const div = document.createElement("div"); if (track.trackID) div.id = track.trackID; mediaContainer.appendChild(div); track.play(div) .catch(e => { alert(e.message, "warning"); }); } } } function handleUserUnPublished(userID, tracks) { for (const track of tracks) { let div = document.getElementById(track.trackID); if (div) div.remove(); } } joinRoomBtn.onclick = async (e) => { e.preventDefault(); try { client.on("user-published", handleUserPublished); client.on("user-unpublished", handleUserUnPublished); await client.join(roomTokenInput.value); const audioTrack = await QNRTC.createBufferSourceAudioTrack({ source: audioFile, tag: "bufferSourceAudio" }); audioTrack.start(); localTracks.push(audioTrack); for (const track of localTracks) { if (track.isVideo()) { playUserTracks([track]); } } await client.publish(localTracks); joinRoomBtn.disabled = true; leaveRoomBtn.disabled = false; } catch (e) { alert(e.message, "warning"); } }; leaveRoomBtn.onclick = (e) => { e.preventDefault(); client.leave() .then(() => { for (const track of localTracks) { track.destroy(); } localTracks = []; mediaContainer.innerHTML = ""; joinRoomBtn.disabled = false; leaveRoomBtn.disabled = true; }) .catch((e) => { alert(e.message, "warning"); }); } ``` -------------------------------- ### Initialize and Join/Leave Video Call Source: https://github.com/pili-engineering/qnrtc-web/blob/master/API-Examples-Web/basicVideoCall/index.html This snippet shows the core logic for joining and leaving a video call. It includes setting up event listeners for user publishing/unpublishing, creating local media tracks, publishing them, and handling the UI state for join/leave buttons. Ensure you have a valid room token. ```javascript const client = QNRTC.createClient(); const joinRoomBtn = document.getElementById("join-room-btn"); const leaveRoomBtn = document.getElementById("leave-room-btn"); const roomTokenInput = document.getElementById("room-token"); const mediaContainer = document.getElementById("media-container"); let localTracks = []; async function handleUserPublished(userID, tracks) { try { const { videoTracks, audioTracks } = await client.subscribe(tracks); playUserTracks([...videoTracks, ...audioTracks]); } catch (e) { alert(e.message, "warning"); } } function playUserTracks(tracks) { for (const track of tracks) { if (track.isAudio()) { track.play(document.body) .catch(e => { alert(e.message, "warning"); }); } else { const div = document.createElement("div"); if (track.trackID) div.id = track.trackID; mediaContainer.appendChild(div); track.play(div) .catch(e => { alert(e.message, "warning"); }); } } } function handleUserUnPublished(userID, tracks) { for (const track of tracks) { let div = document.getElementById(track.trackID); if (div) div.remove(); } } joinRoomBtn.onclick = async (e) => { e.preventDefault(); try { client.on("user-published", handleUserPublished); client.on("user-unpublished", handleUserUnPublished); await client.join(roomTokenInput.value); localTracks = await QNRTC.createMicrophoneAndCameraTracks(); for (const track of localTracks) { if (track.isVideo()) { playUserTracks([track]); } } await client.publish(localTracks); joinRoomBtn.disabled = true; leaveRoomBtn.disabled = false; } catch (e) { alert(e.message, "warning"); } }; leaveRoomBtn.onclick = (e) => { e.preventDefault(); client.leave() .then(() => { for (const track of localTracks) { track.destroy(); } localTracks = []; mediaContainer.innerHTML = ""; joinRoomBtn.disabled = false; leaveRoomBtn.disabled = true; }) .catch((e) => { alert(e.message, "warning"); }); } ``` -------------------------------- ### Join Room and Publish Custom Tracks Source: https://github.com/pili-engineering/qnrtc-web/blob/master/API-Examples-Web/customTrack/index.html This snippet shows how to join a room using a provided token, capture a media stream from a video source, create custom audio and video tracks from the stream's tracks, and publish them to the room. It also handles enabling/disabling join/leave buttons and playing the video. ```javascript const client = QNRTC.createClient(); const joinRoomBtn = document.getElementById("join-room-btn"); const leaveRoomBtn = document.getElementById("leave-room-btn"); const roomTokenInput = document.getElementById("room-token"); const mediaContainer = document.getElementById("media-container"); const videoSource = document.getElementById("video-source"); let localTracks = []; joinRoomBtn.onclick = async (e) => { e.preventDefault(); try { await client.join(roomTokenInput.value); const stream = videoSource.captureStream(); const tracks = stream.getTracks(); for (const track of tracks) { let qntrack; if (track.kind === "audio") { qntrack = QNRTC.createCustomAudioTrack({ mediaStreamTrack: track }); } else { qntrack = QNRTC.createCustomVideoTrack({ mediaStreamTrack: track }); } localTracks.push(qntrack); } await client.publish(localTracks); videoSource.play(); joinRoomBtn.disabled = true; leaveRoomBtn.disabled = false; } catch (e) { alert(e.message, "warning"); } }; ``` -------------------------------- ### Initialize and Join/Leave Room Source: https://github.com/pili-engineering/qnrtc-web/blob/master/API-Examples-Web/pureAudioCall/index.html Initializes the QNRTC client, handles joining a room with a provided token, and publishing local audio tracks. Also includes functionality to leave the room and clean up resources. ```javascript const client = QNRTC.createClient(); const joinRoomBtn = document.getElementById("join-room-btn"); const leaveRoomBtn = document.getElementById("leave-room-btn"); const roomTokenInput = document.getElementById("room-token"); const mediaContainer = document.getElementById("media-container"); let localTracks = []; joinRoomBtn.onclick = async (e) => { e.preventDefault(); try { client.on("user-published", handleUserPublished); await client.join(roomTokenInput.value); const audioTrack = await QNRTC.createMicrophoneAudioTrack(); localTracks.push(audioTrack); await client.publish(localTracks); joinRoomBtn.disabled = true; leaveRoomBtn.disabled = false; } catch (e) { alert(e.message, "warning"); } }; leaveRoomBtn.onclick = (e) => { e.preventDefault(); client.leave() .then(() => { for (const track of localTracks) { track.destroy(); } localTracks = []; mediaContainer.innerHTML = ""; joinRoomBtn.disabled = false; leaveRoomBtn.disabled = true; }) .catch((e) => { alert(e.message, "warning"); }); } ``` -------------------------------- ### Join and Leave Room with Screen Sharing Source: https://github.com/pili-engineering/qnrtc-web/blob/master/API-Examples-Web/screenSharing/index.html This snippet shows how to join a room, publish local tracks including a screen share track, and leave the room. It also handles subscribing to remote user tracks. Ensure you have a valid room token. ```javascript const client = QNRTC.createClient(); const joinRoomBtn = document.getElementById("join-room-btn"); const leaveRoomBtn = document.getElementById("leave-room-btn"); const roomTokenInput = document.getElementById("room-token"); const mediaContainer = document.getElementById("media-container"); let localTracks = []; async function handleUserPublished(userID, tracks) { try { const { videoTracks, audioTracks } = await client.subscribe(tracks); playUserTracks([...videoTracks, ...audioTracks]); } catch (e) { alert(e.message, "warning"); } } function playUserTracks(tracks) { for (const track of tracks) { if (track.isAudio()) { track.play(document.body) .catch(e => { alert(e.message, "warning"); }); } else { const div = document.createElement("div"); if (track.trackID) div.id = track.trackID; mediaContainer.appendChild(div); track.play(div) .catch(e => { alert(e.message, "warning"); }); } } } function handleUserUnPublished(userID, tracks) { for (const track of tracks) { let div = document.getElementById(track.trackID); if (div) div.remove(); } } joinRoomBtn.onclick = async (e) => { e.preventDefault(); try { client.on("user-published", handleUserPublished); client.on("user-unpublished", handleUserUnPublished); // 采集屏幕分享 track const screenTrack = await QNRTC.createScreenVideoTrack(); localTracks = await QNRTC.createMicrophoneAndCameraTracks(); // 监听用户点击了屏幕分享按钮 screenTrack.once("ended", () => { alert("screen track stopped", "warning"); }); localTracks.push(screenTrack); await client.join(roomTokenInput.value); for (const track of localTracks) { if (track.isVideo()) { playUserTracks([track]); } } await client.publish(localTracks); joinRoomBtn.disabled = true; leaveRoomBtn.disabled = false; } catch (e) { alert(e.message, "warning"); } }; leaveRoomBtn.onclick = (e) => { e.preventDefault(); client.leave() .then(() => { for (const track of localTracks) { track.destroy(); } localTracks = []; mediaContainer.innerHTML = ""; joinRoomBtn.disabled = false; leaveRoomBtn.disabled = true; }) .catch((e) => { alert(e.message, "warning"); }); } ``` -------------------------------- ### Join and Publish with Audio Mixing Source: https://github.com/pili-engineering/qnrtc-web/blob/master/API-Examples-Web/audioMixing/index.html This snippet demonstrates joining an RTC room and publishing local tracks. It includes creating an audio mixer to play a remote MP3 file as background music, setting its volume, and publishing the local tracks. Ensure you have the necessary room token and the remote audio file URL. ```javascript const client = QNRTC.createClient(); const joinRoomBtn = document.getElementById("join-room-btn"); const leaveRoomBtn = document.getElementById("leave-room-btn"); const roomTokenInput = document.getElementById("room-token"); const mediaContainer = document.getElementById("media-container"); let localTracks = []; async function handleUserPublished(userID, tracks) { try { const { videoTracks, audioTracks } = await client.subscribe(tracks); playUserTracks([...videoTracks, ...audioTracks]); } catch (e) { alert(e.message, "warning"); } } function playUserTracks(tracks) { for (const track of tracks) { if (track.isAudio()) { track.play(document.body) .catch(e => { alert(e.message, "warning"); }); } else { const div = document.createElement("div"); if (track.trackID) div.id = track.trackID; mediaContainer.appendChild(div); track.play(div) .catch(e => { alert(e.message, "warning"); }); } } } function handleUserUnPublished(userID, tracks) { for (const track of tracks) { let div = document.getElementById(track.trackID); if (div) div.remove(); } } joinRoomBtn.onclick = async (e) => { e.preventDefault(); try { client.on("user-published", handleUserPublished); client.on("user-unpublished", handleUserUnPublished); await client.join(roomTokenInput.value); localTracks = await QNRTC.createMicrophoneAndCameraTracks(); for (const track of localTracks) { if (track.isVideo()) { playUserTracks([track]); } else { const audioMixer = track.createAudioMixer("https://docs.qnsdk.com/eva_edm.mp3"); audioMixer.start(); audioMixer.setPlayingVolume(0.1); } } await client.publish(localTracks); joinRoomBtn.disabled = true; leaveRoomBtn.disabled = false; } catch (e) { alert(e.message, "warning"); } }; ``` -------------------------------- ### Join Room and Publish Tracks with Specific Devices Source: https://github.com/pili-engineering/qnrtc-web/blob/master/API-Examples-Web/switchMediaDevices/index.html Joins a room using a provided token and publishes local audio/video tracks. Allows specifying the exact microphone and camera device IDs to use for track creation. If no device ID is specified, the default device is used. ```javascript let localTracks = []; async function handleUserPublished(userID, tracks) { try { const { videoTracks, audioTracks } = await client.subscribe(tracks); playUserTracks([...videoTracks, ...audioTracks]); } catch (e) { alert(e.message, "warning"); } } function playUserTracks(tracks) { for (const track of tracks) { if (track.isAudio()) { track.play(document.body) .then(() => { if (playbackDeviceSelect.value !== "-") { return track.setPlaybackDevice(playbackDeviceSelect.value); } }) .catch(e => { alert(e.message, "warning"); }); } else { const div = document.createElement("div"); if (track.trackID) div.id = track.trackID; mediaContainer.appendChild(div); track.play(div) .catch(e => { alert(e.message, "warning"); }); } } } function handleUserUnPublished(userID, tracks) { for (const track of tracks) { let div = document.getElementById(track.trackID); if (div) div.remove(); } } joinRoomBtn.onclick = async (e) => { e.preventDefault(); try { client.on("user-published", handleUserPublished); client.on("user-unpublished", handleUserUnPublished); await client.join(roomTokenInput.value); // 根据指定的设备 id 来完成采集 localTracks = await QNRTC.createMicrophoneAndCameraTracks( { microphoneId: microphoneSelect.value === "-" ? undefined : microphoneSelect.value }, { cameraId: cameraSelect.value === "-" ? undefined : cameraSelect.value }, ); for (const track of localTracks) { if (track.isVideo()) { playUserTracks([track]); } } await client.publish(localTracks); joinRoomBtn.disabled = true; leaveRoomBtn.disabled = false; } catch (e) { alert(e.message, "warning"); } }; ``` -------------------------------- ### Join and Publish with Dual Stream Source: https://github.com/pili-engineering/qnrtc-web/blob/master/API-Examples-Web/videoDualStream/index.html Joins a room and publishes local audio and video tracks. The video track is configured with dual-stream encoding, specifying resolution and bitrate for both the main and low-stream. This snippet requires a room token and sets up event listeners for user publishing and unpublishing. ```javascript const client = QNRTC.createClient(); const joinRoomBtn = document.getElementById("join-room-btn"); const leaveRoomBtn = document.getElementById("leave-room-btn"); const roomTokenInput = document.getElementById("room-token"); const mediaContainer = document.getElementById("media-container"); let localTracks = []; // 存储开启了大小流的远端 video track let remoteVideoTracks = new Map(); async function handleUserPublished(userID, tracks) { try { const { videoTracks, audioTracks } = await client.subscribe(tracks); playUserTracks([...videoTracks, ...audioTracks]); for (const track of videoTracks) { if (track.isLowStreamEnabled) { track.on("low-stream-state-changed", state => { console.log("state: ", track.trackID, state); }); remoteVideoTracks.set(track.trackID, track); } } } catch (e) { alert(e.message, "warning"); } } function handleUserUnPublished(userID, tracks) { for (const track of tracks) { if (track.isVideo() && track.isLowStreamEnabled) { remoteVideoTracks.delete(track.trackID); const div = document.getElementById(track.trackID); if (div) div.remove(); } } } function handleSetLowStream(checked, trackID) { const track = remoteVideoTracks.get(trackID); if (!track) { alert("remote track not found", trackID); return; } track.setLowStream(checked); } function createVideoPlayContainer(trackID) { const div = document.createElement("div"); div.style.position = "relative"; div.id = trackID; div.innerHTML = `