This commit is contained in:
zhangzheng
2026-02-27 18:35:40 +08:00
parent adef8b4cce
commit 1bb1fee5cc
265 changed files with 104076 additions and 92 deletions

View File

@@ -0,0 +1,54 @@
div#select, div#resolution {
margin: 1em;
}
button {
margin: 0 20px 5px 0;
vertical-align: top;
width: 155px;
}
div#buttons {
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
margin: 1em 0 1em 0;
padding: 1em 0 1em 0;
}
div#local {
margin: 0 20px 0 0;
}
div#preview {
border-bottom: 1px solid #eee;
margin: 0 0 1em 0;
padding: 0 0 0.5em 0;
}
div#preview>div {
display: inline-block;
vertical-align: top;
width: calc(50% - 20px);
}
div#connectionId {
margin: 1em;
}
h2 {
margin: 0 0 0.5em 0;
}
textarea {
color: #444;
font-size: 0.9em;
font-weight: 300;
width: calc(20% - 10px);
height: 1.3em;
line-height: 1.3;
vertical-align: middle;
}
video {
height: 225px;
}

View File

@@ -0,0 +1,83 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="icon" href="../images/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="../css/main.css" />
<link rel="stylesheet" href="css/style.css" />
<title>Bidirectional Sample</title>
</head>
<body>
<div id="container">
<h1>Bidirectional Sample</h1>
<div id="warning" hidden=true></div>
<div id="select">
<label for="videoSource">Video source: </label>
<select id="videoSource" autocomplete="off"></select>
<label for="audioSource">Audio source: </label>
<select id="audioSource" autocomplete="off"></select>
</div>
<div id="resolutionSelect">
<label for="videoResolution">Video resolution: </label><select id="videoResolution" autocomplete="off"></select>
</div>
<div id="resolutionInput">
<label for="cameraWidth">Camera width:</label><input id="cameraWidth" type="number" min="0" max="4096" autocomplete="off" disabled>
<label for="cameraHeight">Camera height:</label><input id="cameraHeight" type="number" min="0" max="4096" autocomplete="off" disabled>
</div>
<div id="buttons">
<button type="button" id="startVideoButton" autocomplete="off">Start Video</button>
<button type="button" id="setUpButton" autocomplete="off" disabled>Set Up</button>
<button type="button" id="hangUpButton" autocomplete="off" disabled>Hang Up</button>
</div>
<div id="preview">
<div id="local">
<h2>Local</h2>
<video id="localVideo" playsinline autoplay muted=true></video>
<div id="localVideoStats"></div>
</div>
<div id="remote">
<h2>Remote</h2>
<video id="remoteVideo" playsinline autoplay></video>
<div id="remoteVideoStats"></div>
</div>
</div>
<div class="box">
<span>Connection ID:</span>
<textarea id="textForConnectionId"></textarea>
</div>
<div class="box">
<span>Codec preferences:</span>
<select id="codecPreferences" autocomplete="off" disabled>
<option selected value="">Default</option>
</select>
</div>
<p>For more information about <code>Bidirectional</code> sample, see <a
href="https://docs.unity3d.com/Packages/com.unity.renderstreaming@3.1/manual/sample-bidirectional.html">Bidirectional
sample</a> document page.</p>
<div id="message"></div>
<section>
<a href="https://github.com/Unity-Technologies/UnityRenderStreaming/tree/develop/WebApp/public/bidirectional"
title="View source for this page on GitHub" id="viewSource">View source on GitHub</a>
</section>
</div>
</body>
<script type="text/javascript" src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="https://unpkg.com/event-target@latest/min.js"></script>
<script src="https://unpkg.com/resize-observer-polyfill@1.5.0/dist/ResizeObserver.global.js"></script>
<script type="module" src="js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,289 @@
import { SendVideo } from "./sendvideo.js";
import { getServerConfig, getRTCConfiguration } from "../../js/config.js";
import { createDisplayStringArray } from "../../js/stats.js";
import { RenderStreaming } from "../../module/renderstreaming.js";
import { Signaling, WebSocketSignaling } from "../../module/signaling.js";
const defaultStreamWidth = 1280;
const defaultStreamHeight = 720;
const streamSizeList =
[
{ width: 640, height: 360 },
{ width: 1280, height: 720 },
{ width: 1920, height: 1080 },
{ width: 2560, height: 1440 },
{ width: 3840, height: 2160 },
{ width: 360, height: 640 },
{ width: 720, height: 1280 },
{ width: 1080, height: 1920 },
{ width: 1440, height: 2560 },
{ width: 2160, height: 3840 },
];
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
const localVideoStatsDiv = document.getElementById('localVideoStats');
const remoteVideoStatsDiv = document.getElementById('remoteVideoStats');
const textForConnectionId = document.getElementById('textForConnectionId');
textForConnectionId.value = getRandom();
const videoSelect = document.querySelector('select#videoSource');
const audioSelect = document.querySelector('select#audioSource');
const videoResolutionSelect = document.querySelector('select#videoResolution');
const cameraWidthInput = document.querySelector('input#cameraWidth');
const cameraHeightInput = document.querySelector('input#cameraHeight');
const codecPreferences = document.getElementById('codecPreferences');
const supportsSetCodecPreferences = window.RTCRtpTransceiver &&
'setCodecPreferences' in window.RTCRtpTransceiver.prototype;
const messageDiv = document.getElementById('message');
messageDiv.style.display = 'none';
let useCustomResolution = false;
setUpInputSelect();
showCodecSelect();
/** @type {SendVideo} */
let sendVideo = new SendVideo(localVideo, remoteVideo);
/** @type {RenderStreaming} */
let renderstreaming;
let useWebSocket;
let connectionId;
const startButton = document.getElementById('startVideoButton');
startButton.addEventListener('click', startVideo);
const setupButton = document.getElementById('setUpButton');
setupButton.addEventListener('click', setUp);
const hangUpButton = document.getElementById('hangUpButton');
hangUpButton.addEventListener('click', hangUp);
window.addEventListener('beforeunload', async () => {
if(!renderstreaming)
return;
await renderstreaming.stop();
}, true);
setupConfig();
async function setupConfig() {
const res = await getServerConfig();
useWebSocket = res.useWebSocket;
showWarningIfNeeded(res.startupMode);
}
function showWarningIfNeeded(startupMode) {
const warningDiv = document.getElementById("warning");
if (startupMode == "public") {
warningDiv.innerHTML = "<h4>Warning</h4> This sample is not working on Public Mode.";
warningDiv.hidden = false;
}
}
async function startVideo() {
videoSelect.disabled = true;
audioSelect.disabled = true;
videoResolutionSelect.disabled = true;
cameraWidthInput.disabled = true;
cameraHeightInput.disabled = true;
startButton.disabled = true;
let width = 0;
let height = 0;
if (useCustomResolution) {
width = cameraWidthInput.value ? cameraWidthInput.value : defaultStreamWidth;
height = cameraHeightInput.value ? cameraHeightInput.value : defaultStreamHeight;
} else {
const size = streamSizeList[videoResolutionSelect.value];
width = size.width;
height = size.height;
}
await sendVideo.startLocalVideo(videoSelect.value, audioSelect.value, width, height);
// enable setup button after initializing local video.
setupButton.disabled = false;
}
async function setUp() {
setupButton.disabled = true;
hangUpButton.disabled = false;
connectionId = textForConnectionId.value;
codecPreferences.disabled = true;
const signaling = useWebSocket ? new WebSocketSignaling() : new Signaling();
const config = getRTCConfiguration();
renderstreaming = new RenderStreaming(signaling, config);
renderstreaming.onConnect = () => {
const tracks = sendVideo.getLocalTracks();
for (const track of tracks) {
renderstreaming.addTransceiver(track, { direction: 'sendonly' });
}
setCodecPreferences();
showStatsMessage();
};
renderstreaming.onDisconnect = () => {
hangUp();
};
renderstreaming.onTrackEvent = (data) => {
const direction = data.transceiver.direction;
if (direction == "sendrecv" || direction == "recvonly") {
sendVideo.addRemoteTrack(data.track);
}
};
await renderstreaming.start();
await renderstreaming.createConnection(connectionId);
}
// 获取浏览器麦克风并发送到 Unity
function setCodecPreferences() {
/** @type {RTCRtpCodecCapability[] | null} */
let selectedCodecs = null;
if (supportsSetCodecPreferences) {
const preferredCodec = codecPreferences.options[codecPreferences.selectedIndex];
if (preferredCodec.value !== '') {
const [mimeType, sdpFmtpLine] = preferredCodec.value.split(' ');
const { codecs } = RTCRtpSender.getCapabilities('video');
const selectedCodecIndex = codecs.findIndex(c => c.mimeType === mimeType && c.sdpFmtpLine === sdpFmtpLine);
const selectCodec = codecs[selectedCodecIndex];
selectedCodecs = [selectCodec];
}
}
if (selectedCodecs == null) {
return;
}
const transceivers = renderstreaming.getTransceivers().filter(t => t.receiver.track.kind == "video");
if (transceivers && transceivers.length > 0) {
transceivers.forEach(t => t.setCodecPreferences(selectedCodecs));
}
}
async function hangUp() {
clearStatsMessage();
messageDiv.style.display = 'block';
messageDiv.innerText = `Disconnect peer on ${connectionId}.`;
hangUpButton.disabled = true;
setupButton.disabled = false;
await renderstreaming.deleteConnection();
await renderstreaming.stop();
renderstreaming = null;
remoteVideo.srcObject = null;
textForConnectionId.value = getRandom();
connectionId = null;
if (supportsSetCodecPreferences) {
codecPreferences.disabled = false;
}
}
function getRandom() {
const max = 99999;
const length = String(max).length;
const number = Math.floor(Math.random() * max);
return (Array(length).join('0') + number).slice(-length);
}
async function setUpInputSelect() {
const deviceInfos = await navigator.mediaDevices.enumerateDevices();
for (let i = 0; i !== deviceInfos.length; ++i) {
const deviceInfo = deviceInfos[i];
if (deviceInfo.kind === 'videoinput') {
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`;
videoSelect.appendChild(option);
} else if (deviceInfo.kind === 'audioinput') {
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
option.text = deviceInfo.label || `mic ${audioSelect.length + 1}`;
audioSelect.appendChild(option);
}
}
for (let i = 0; i < streamSizeList.length; i++) {
const streamSize = streamSizeList[i];
const option = document.createElement('option');
option.value = i;
option.text = `${streamSize.width} x ${streamSize.height}`;
videoResolutionSelect.appendChild(option);
}
const option = document.createElement('option');
option.value = streamSizeList.length;
option.text = 'Custom';
videoResolutionSelect.appendChild(option);
videoResolutionSelect.value = 1; // default select index (1280 x 720)
videoResolutionSelect.addEventListener('change', (event) => {
const isCustom = event.target.value >= streamSizeList.length;
cameraWidthInput.disabled = !isCustom;
cameraHeightInput.disabled = !isCustom;
useCustomResolution = isCustom;
});
}
function showCodecSelect() {
if (!supportsSetCodecPreferences) {
messageDiv.style.display = 'block';
messageDiv.innerHTML = `Current Browser does not support <a href="https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpTransceiver/setCodecPreferences">RTCRtpTransceiver.setCodecPreferences</a>.`;
return;
}
const codecs = RTCRtpSender.getCapabilities('video').codecs;
codecs.forEach(codec => {
if (['video/red', 'video/ulpfec', 'video/rtx'].includes(codec.mimeType)) {
return;
}
const option = document.createElement('option');
option.value = (codec.mimeType + ' ' + (codec.sdpFmtpLine || '')).trim();
option.innerText = option.value;
codecPreferences.appendChild(option);
});
codecPreferences.disabled = false;
}
let lastStats;
let intervalId;
function showStatsMessage() {
intervalId = setInterval(async () => {
if (localVideo.videoWidth) {
localVideoStatsDiv.innerHTML = `<strong>Sending resolution:</strong> ${localVideo.videoWidth} x ${localVideo.videoHeight} px`;
}
if (remoteVideo.videoWidth) {
remoteVideoStatsDiv.innerHTML = `<strong>Receiving resolution:</strong> ${remoteVideo.videoWidth} x ${remoteVideo.videoHeight} px`;
}
if (renderstreaming == null || connectionId == null) {
return;
}
const stats = await renderstreaming.getStats();
if (stats == null) {
return;
}
const array = createDisplayStringArray(stats, lastStats);
if (array.length) {
messageDiv.style.display = 'block';
messageDiv.innerHTML = array.join('<br>');
}
lastStats = stats;
}, 1000);
}
function clearStatsMessage() {
if (intervalId) {
clearInterval(intervalId);
}
lastStats = null;
intervalId = null;
localVideoStatsDiv.innerHTML = '';
remoteVideoStatsDiv.innerHTML = '';
messageDiv.style.display = 'none';
messageDiv.innerHTML = '';
}

View File

@@ -0,0 +1,53 @@
import * as Logger from "../../module/logger.js";
export class SendVideo {
constructor(localVideoElement, remoteVideoElement) {
this.localVideo = localVideoElement;
this.remoteVideo = remoteVideoElement;
}
/**
* @param {MediaTrackConstraints} videoSource
* @param {MediaTrackConstraints} audioSource
* @param {number} videoWidth
* @param {number} videoHeight
*/
async startLocalVideo(videoSource, audioSource, videoWidth, videoHeight) {
try {
const constraints = {
video: { deviceId: videoSource ? { exact: videoSource } : undefined },
audio: { deviceId: audioSource ? { exact: audioSource } : undefined }
};
if (videoWidth != null || videoWidth != 0) {
constraints.video.width = videoWidth;
}
if (videoHeight != null || videoHeight != 0) {
constraints.video.height = videoHeight;
}
const localStream = await navigator.mediaDevices.getUserMedia(constraints);
this.localVideo.srcObject = localStream;
await this.localVideo.play();
} catch (err) {
Logger.error(`mediaDevice.getUserMedia() error:${err}`);
}
}
/**
* @returns {MediaStreamTrack[]}
*/
getLocalTracks() {
return this.localVideo.srcObject.getTracks();
}
/**
* @param {MediaStreamTrack} track
*/
addRemoteTrack(track) {
if (this.remoteVideo.srcObject == null) {
this.remoteVideo.srcObject = new MediaStream();
}
this.remoteVideo.srcObject.addTrack(track);
}
}

View File

@@ -0,0 +1,147 @@
h1 {
border-bottom: 1px solid #ccc;
font-weight: 500;
margin: 0 0 0.8em 0;
padding: 0 0 0.2em 0;
}
h4 {
margin: 0;
padding: 0 0 0.2em 0;
}
body {
font-family: 'Roboto', sans-serif;
font-weight: 300;
margin: 0;
padding: 1em;
word-break: break-word;
}
button {
margin: 20px 10px 0 0;
width: 130px;
}
button#gather {
display: block;
}
section {
border-bottom: 1px solid #eee;
margin: 0 0 1.5em 0;
padding: 0 0 1.5em 0;
}
section#iceServers label {
display: inline-block;
width: 150px;
}
section#iceServers input {
margin: 0 0 10px;
width: 260px;
}
select {
margin: 0 1em 1em 0;
position: relative;
top: -1px;
}
select#servers {
font-size: 1em;
padding: 5px;
width: 420px;
}
section:last-child {
border-bottom: none;
margin: 0;
padding: 0;
}
div#container {
margin: 0 auto 0 auto;
max-width: 60em;
padding: 1em 1.5em 1.3em 1.5em;
}
code {
padding: 0.1em 0.25em;
color: #444;
background-color: #e7edf3;
border-radius: 3px;
border: solid 1px #d6dde4;
font-weight: 400;
}
p {
color: #444;
font-weight: 300;
}
p#data {
border-top: 1px dotted #666;
line-height: 1.3em;
max-height: 1000px;
overflow-y: auto;
padding: 1em 0 0 0;
}
p.borderBelow {
border-bottom: 1px solid #aaa;
padding: 0 0 20px 0;
}
video {
background: #222;
margin: 0 0 20px 0;
--width: 100%;
width: var(--width);
height: calc(var(--width) * 0.75);
}
div#warning {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc;
padding: 1em;
border: 1px solid transparent;
}
div.box {
margin: 1em;
}
div#message {
border-top: 1px solid #666;
margin: 1em;
padding: 1em;
}
@media screen and (max-width: 650px) {
.highlight {
font-size: 1em;
margin: 0 0 20px 0;
padding: 0.2em 1em;
}
h1 {
font-size: 24px;
}
}
@media screen and (max-width: 550px) {
button:active {
background-color: darkRed;
}
h1 {
font-size: 22px;
}
}
@media screen and (max-width: 450px) {
h1 {
font-size: 20px;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,77 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="icon" href="images/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="css/main.css" />
<title>Unity Render Streaming Samples</title>
</head>
<body>
<div id="container">
<h1>Unity Render Streaming Samples</h1>
<section>
<p>These are WebClient samples for use with <a href="https://docs.unity3d.com/Packages/com.unity.renderstreaming@latest/index.html">Unity Render
Streaming</a>.</p>
</section>
<section>
<h2>Server Configuration</h2>
<div id="startup"></div>
</section>
<section id="iceServers">
<h2>ICE servers</h2>
<select id="servers" size="4">
</select>
<div>
<label for="url">STUN or TURN URI:</label>
<input id="url">
</div>
<div>
<label for="username">TURN username:</label>
<input id="username">
</div>
<div>
<label for="password">TURN password:</label>
<input id="password">
</div>
<div>
<button id="add">Add Server</button>
<button id="remove">Remove Server</button>
<button id="reset">Reset to defaults</button>
</div>
</section>
<section>
<h2 id="receiver"><a href="receiver/index.html">Receiver Sample</a></h2>
<p>This is a sample for receiving video / audio from Unity.</p>
<p>It can be used in combination with the <code>Broadcast</code> scene of Unity Render Streaming.</p>
</section>
<section>
<h2 id="bidirectional"><a href="bidirectional/index.html">Bidirectional Sample</a></h2>
<p>This is a sample for sending and receiving video in both directions.</p>
<p>It can be used in combination with the <code>Bidirectional</code> scene of Unity Render Streaming.</p>
<p>The WebApp must be running in Private mode.</p>
</section>
<section>
<h2 id="multiplay"><a href="multiplay/index.html">Multiplay Sample</a></h2>
<p>This sample connects as a Guest in the <code>Multiplay</code> scene of Unity Render Streaming.</p>
</section>
<section>
<h2 id="videoplayer"><a href="videoplayer/index.html">VideoPlayer Sample</a></h2>
<p>This is a sample to receive the camera image rendered on Unity. You can operate the camera in Unity from the
browser.</p>
<p>It can be used in combination with the <code>WebBrowserInput</code> scene of Unity Render Streaming.</p>
<p>The WebApp must be running in Public mode.</p>
</section>
<section>
<a href="https://github.com/Unity-Technologies/UnityRenderStreaming/tree/develop/WebApp" title="View source for this page on GitHub" id="viewSource">View source on GitHub</a>
</section>
</div>
<script type="module" src="js/main.js"></script>
</body>

View File

@@ -0,0 +1,14 @@
import {getServers} from "./icesettings.js";
export async function getServerConfig() {
const protocolEndPoint = location.origin + '/config';
const createResponse = await fetch(protocolEndPoint);
return await createResponse.json();
}
export function getRTCConfiguration() {
let config = {};
config.sdpSemantics = 'unified-plan';
config.iceServers = getServers();
return config;
}

View File

@@ -0,0 +1,103 @@
// This code is referenced from webrtc sample.
// https://github.com/webrtc/samples/blob/gh-pages/src/content/peerconnection/trickle-ice/js/main.js
const servers = document.querySelector('select#servers');
const urlInput = document.querySelector('input#url');
const usernameInput = document.querySelector('input#username');
const passwordInput = document.querySelector('input#password');
const allServersKey = 'servers';
export function addServer() {
const scheme = urlInput.value.split(':')[0];
if (!['stun', 'stuns', 'turn', 'turns'].includes(scheme)) {
alert(`URI scheme ${scheme} is not valid`);
return;
}
// Store the ICE server as a stringified JSON object in option.value.
const option = document.createElement('option');
const iceServer = {
urls: [urlInput.value],
username: usernameInput.value,
credential: passwordInput.value
};
option.value = JSON.stringify(iceServer);
option.text = `${urlInput.value} `;
const username = usernameInput.value;
const password = passwordInput.value;
if (username || password) {
option.text += (` [${username}:${password}]`);
}
option.ondblclick = selectServer;
servers.add(option);
urlInput.value = usernameInput.value = passwordInput.value = '';
writeServersToLocalStorage();
}
export function removeServer() {
for (let i = servers.options.length - 1; i >= 0; --i) {
if (servers.options[i].selected) {
servers.remove(i);
}
}
writeServersToLocalStorage();
}
export function reset() {
window.localStorage.clear();
document.querySelectorAll('select#servers option').forEach(option => option.remove());
const serversSelect = document.querySelector('select#servers');
setDefaultServer(serversSelect);
}
function selectServer(event) {
const option = event.target;
const value = JSON.parse(option.value);
urlInput.value = value.urls[0];
usernameInput.value = value.username || '';
passwordInput.value = value.credential || '';
}
function setDefaultServer(serversSelect) {
const option = document.createElement('option');
option.value = '{"urls":["stun:stun.l.google.com:19302"]}';
option.text = 'stun:stun.l.google.com:19302';
option.ondblclick = selectServer;
serversSelect.add(option);
}
function writeServersToLocalStorage() {
const serversSelect = document.querySelector('select#servers');
const allServers = JSON.stringify(Object.values(serversSelect.options).map(o => JSON.parse(o.value)));
window.localStorage.setItem(allServersKey, allServers);
}
export function readServersFromLocalStorage() {
document.querySelectorAll('select#servers option').forEach(option => option.remove());
const serversSelect = document.querySelector('select#servers');
const storedServers = window.localStorage.getItem(allServersKey);
if (storedServers === null || storedServers === '') {
setDefaultServer(serversSelect);
} else {
JSON.parse(storedServers).forEach((server) => {
const o = document.createElement('option');
o.value = JSON.stringify(server);
o.text = server.urls[0];
o.ondblclick = selectServer;
serversSelect.add(o);
});
}
}
export function getServers() {
const storedServers = window.localStorage.getItem(allServersKey);
if (storedServers === null || storedServers === '') {
return [{ urls: ['stun:stun.l.google.com:19302'] }];
}
else {
return JSON.parse(storedServers);
}
}

View File

@@ -0,0 +1,27 @@
import * as Config from "./config.js";
import {addServer, removeServer, reset, readServersFromLocalStorage} from "./icesettings.js";
const addButton = document.querySelector('button#add');
const removeButton = document.querySelector('button#remove');
const resetButton = document.querySelector('button#reset');
const startupDiv = document.getElementById("startup");
addButton.onclick = addServer;
removeButton.onclick = removeServer;
resetButton.onclick = reset;
startupDiv.innerHTML = "";
const displayConfig = async () => {
const res = await Config.getServerConfig();
if (res.useWebSocket) {
startupDiv.innerHTML += "<li>Signaling Protocol : <b>WebSocket</b></li>";
} else {
startupDiv.innerHTML += "<li>Signaling Protocol : <b>HTTP</b></li>";
}
const mode = res.startupMode.replace(/^./, res.startupMode[0].toUpperCase());
startupDiv.innerHTML += `<li>Signaling Mode : <b>${mode}</b></li>`;
};
displayConfig();
readServersFromLocalStorage();

View File

@@ -0,0 +1,91 @@
/**
* create display string array from RTCStatsReport
* @param {RTCStatsReport} report - current RTCStatsReport
* @param {RTCStatsReport} lastReport - latest RTCStatsReport
* @return {Array<string>} - display string Array
*/
export function createDisplayStringArray(report, lastReport) {
let array = new Array();
report.forEach(stat => {
if (stat.type === 'inbound-rtp') {
array.push(`${stat.kind} receiving stream stats`);
if (stat.codecId != undefined) {
const codec = report.get(stat.codecId);
array.push(`Codec: ${codec.mimeType}`);
if (codec.sdpFmtpLine) {
codec.sdpFmtpLine.split(";").forEach(fmtp => {
array.push(` - ${fmtp}`);
});
}
if (codec.payloadType) {
array.push(` - payloadType=${codec.payloadType}`);
}
if (codec.clockRate) {
array.push(` - clockRate=${codec.clockRate}`);
}
if (codec.channels) {
array.push(` - channels=${codec.channels}`);
}
}
if (stat.kind == "video") {
array.push(`Decoder: ${stat.decoderImplementation}`);
array.push(`Resolution: ${stat.frameWidth}x${stat.frameHeight}`);
array.push(`Framerate: ${stat.framesPerSecond}`);
}
if (lastReport && lastReport.has(stat.id)) {
const lastStats = lastReport.get(stat.id);
const duration = (stat.timestamp - lastStats.timestamp) / 1000;
const bitrate = (8 * (stat.bytesReceived - lastStats.bytesReceived) / duration) / 1000;
array.push(`Bitrate: ${bitrate.toFixed(2)} kbit/sec`);
}
} else if (stat.type === 'outbound-rtp') {
array.push(`${stat.kind} sending stream stats`);
if (stat.codecId != undefined) {
const codec = report.get(stat.codecId);
array.push(`Codec: ${codec.mimeType}`);
if (codec.sdpFmtpLine) {
codec.sdpFmtpLine.split(";").forEach(fmtp => {
array.push(` - ${fmtp}`);
});
}
if (codec.payloadType) {
array.push(` - payloadType=${codec.payloadType}`);
}
if (codec.clockRate) {
array.push(` - clockRate=${codec.clockRate}`);
}
if (codec.channels) {
array.push(` - channels=${codec.channels}`);
}
}
if (stat.kind == "video") {
array.push(`Encoder: ${stat.encoderImplementation}`);
array.push(`Resolution: ${stat.frameWidth}x${stat.frameHeight}`);
array.push(`Framerate: ${stat.framesPerSecond}`);
}
if (lastReport && lastReport.has(stat.id)) {
const lastStats = lastReport.get(stat.id);
const duration = (stat.timestamp - lastStats.timestamp) / 1000;
const bitrate = (8 * (stat.bytesSent - lastStats.bytesSent) / duration) / 1000;
array.push(`Bitrate: ${bitrate.toFixed(2)} kbit/sec`);
}
}
});
return array;
}

View File

@@ -0,0 +1,213 @@
import { Observer, Sender } from "../module/sender.js";
import { InputRemoting } from "../module/inputremoting.js";
export class VideoPlayer {
constructor() {
this.playerElement = null;
this.lockMouseCheck = null;
this.videoElement = null;
this.fullScreenButtonElement = null;
this.inputRemoting = null;
this.sender = null;
this.inputSenderChannel = null;
}
/**
* @param {Element} playerElement parent element for create video player
* @param {HTMLInputElement} lockMouseCheck use checked propety for lock mouse
*/
createPlayer(playerElement, lockMouseCheck) {
this.playerElement = playerElement;
this.lockMouseCheck = lockMouseCheck;
this.videoElement = document.createElement('video');
this.videoElement.id = 'Video';
this.videoElement.style.touchAction = 'none';
this.videoElement.playsInline = true;
this.videoElement.srcObject = new MediaStream();
this.videoElement.addEventListener('loadedmetadata', this._onLoadedVideo.bind(this), true);
this.playerElement.appendChild(this.videoElement);
// add fullscreen button
this.fullScreenButtonElement = document.createElement('img');
this.fullScreenButtonElement.id = 'fullscreenButton';
this.fullScreenButtonElement.src = '../images/FullScreen.png';
this.fullScreenButtonElement.addEventListener("click", this._onClickFullscreenButton.bind(this));
this.playerElement.appendChild(this.fullScreenButtonElement);
document.addEventListener('webkitfullscreenchange', this._onFullscreenChange.bind(this));
document.addEventListener('fullscreenchange', this._onFullscreenChange.bind(this));
this.videoElement.addEventListener("click", this._mouseClick.bind(this), false);
}
_onLoadedVideo() {
this.videoElement.play();
this.resizeVideo();
}
_onClickFullscreenButton() {
if (!document.fullscreenElement || !document.webkitFullscreenElement) {
if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen();
}
else if (document.documentElement.webkitRequestFullscreen) {
document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
} else {
if (this.playerElement.style.position == "absolute") {
this.playerElement.style.position = "relative";
} else {
this.playerElement.style.position = "absolute";
}
}
}
}
_onFullscreenChange() {
if (document.webkitFullscreenElement || document.fullscreenElement) {
this.playerElement.style.position = "absolute";
this.fullScreenButtonElement.style.display = 'none';
if (this.lockMouseCheck.checked) {
if (document.webkitFullscreenElement.requestPointerLock) {
document.webkitFullscreenElement.requestPointerLock();
} else if (document.fullscreenElement.requestPointerLock) {
document.fullscreenElement.requestPointerLock();
} else if (document.mozFullScreenElement.requestPointerLock) {
document.mozFullScreenElement.requestPointerLock();
}
// Subscribe to events
document.addEventListener('mousemove', this._mouseMove.bind(this), false);
document.addEventListener('click', this._mouseClickFullScreen.bind(this), false);
}
}
else {
this.playerElement.style.position = "relative";
this.fullScreenButtonElement.style.display = 'block';
document.removeEventListener('mousemove', this._mouseMove.bind(this), false);
document.removeEventListener('click', this._mouseClickFullScreen.bind(this), false);
}
}
_mouseMove(event) {
// Forward mouseMove event of fullscreen player directly to sender
// This is required, as the regular mousemove event doesn't fire when in fullscreen mode
this.sender._onMouseEvent(event);
}
_mouseClick() {
// Restores pointer lock when we unfocus the player and click on it again
if (this.lockMouseCheck.checked) {
if (this.videoElement.requestPointerLock) {
this.videoElement.requestPointerLock().catch(function () { });
}
}
}
_mouseClickFullScreen() {
// Restores pointer lock when we unfocus the fullscreen player and click on it again
if (this.lockMouseCheck.checked) {
if (document.webkitFullscreenElement.requestPointerLock) {
document.webkitFullscreenElement.requestPointerLock();
} else if (document.fullscreenElement.requestPointerLock) {
document.fullscreenElement.requestPointerLock();
} else if (document.mozFullScreenElement.requestPointerLock) {
document.mozFullScreenElement.requestPointerLock();
}
}
}
/**
* @param {MediaStreamTrack} track
*/
addTrack(track) {
if (!this.videoElement.srcObject) {
return;
}
this.videoElement.srcObject.addTrack(track);
}
resizeVideo() {
if (!this.videoElement) {
return;
}
const clientRect = this.videoElement.getBoundingClientRect();
const videoRatio = this.videoWidth / this.videoHeight;
const clientRatio = clientRect.width / clientRect.height;
this._videoScale = videoRatio > clientRatio ? clientRect.width / this.videoWidth : clientRect.height / this.videoHeight;
const videoOffsetX = videoRatio > clientRatio ? 0 : (clientRect.width - this.videoWidth * this._videoScale) * 0.5;
const videoOffsetY = videoRatio > clientRatio ? (clientRect.height - this.videoHeight * this._videoScale) * 0.5 : 0;
this._videoOriginX = clientRect.left + videoOffsetX;
this._videoOriginY = clientRect.top + videoOffsetY;
}
get videoWidth() {
return this.videoElement.videoWidth;
}
get videoHeight() {
return this.videoElement.videoHeight;
}
get videoOriginX() {
return this._videoOriginX;
}
get videoOriginY() {
return this._videoOriginY;
}
get videoScale() {
return this._videoScale;
}
deletePlayer() {
if (this.inputRemoting) {
this.inputRemoting.stopSending();
}
this.inputRemoting = null;
this.sender = null;
this.inputSenderChannel = null;
while (this.playerElement.firstChild) {
this.playerElement.removeChild(this.playerElement.firstChild);
}
this.playerElement = null;
this.lockMouseCheck = null;
}
_isTouchDevice() {
return (('ontouchstart' in window) ||
(navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints > 0));
}
/**
* setup datachannel for player input (muouse/keyboard/touch/gamepad)
* @param {RTCDataChannel} channel
*/
setupInput(channel) {
this.sender = new Sender(this.videoElement);
this.sender.addMouse();
this.sender.addKeyboard();
if (this._isTouchDevice()) {
this.sender.addTouchscreen();
}
this.sender.addGamepad();
this.inputRemoting = new InputRemoting(this.sender);
this.inputSenderChannel = channel;
this.inputSenderChannel.onopen = this._onOpenInputSenderChannel.bind(this);
this.inputRemoting.subscribe(new Observer(this.inputSenderChannel));
}
async _onOpenInputSenderChannel() {
await new Promise(resolve => setTimeout(resolve, 100));
this.inputRemoting.startSending();
}
}

View File

@@ -0,0 +1,99 @@
body {
margin: 0px;
}
#player {
position: relative;
top: 0;
right: 0;
bottom: 0;
left: 0;
align-items: center;
justify-content: center;
display: flex;
background-color: #323232;
}
#player:before {
content: "";
display: block;
padding-top: 66%;
}
#playButton {
width: 15%;
max-width: 200px;
cursor: pointer;
}
#Video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#VideoThumbnail {
position: absolute;
top: 0;
left: 0;
width: 30%;
height: 30%;
}
#greenButton {
position: absolute;
bottom: 10px;
left: 10px;
width: 160px;
background-color: #4CAF50;
/* Green */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
#blueButton {
position: absolute;
bottom: 10px;
left: 180px;
width: 160px;
background-color: #447FAF;
/* Blue */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
#orangeButton {
position: absolute;
bottom: 10px;
left: 350px;
width: 160px;
background-color: #FF7700;
/* Blue */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
#fullscreenButton {
position: absolute;
top: 25px;
right: 25px;
width: 32px;
height: 32px;
}

View File

@@ -0,0 +1,53 @@
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="icon" href="../images/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="../css/main.css" />
<link rel="stylesheet" href="css/style.css" />
<title>Multiplay Sample</title>
</head>
<body>
<div id="container">
<h1>Multiplay Sample</h1>
<div id="warning" hidden="true"></div>
<div id="player"></div>
<div class="box">
<span>Codec preferences:</span>
<select id="codecPreferences" autocomplete="off" disabled>
<option selected value="">Default</option>
</select>
</div>
<div class="box">
<span>Lock Cursor to Player:</span>
<input type="checkbox" id="lockMouseCheck" autocomplete="off" />
</div>
<p>
For more information about sample, see <a
href="https://docs.unity3d.com/Packages/com.unity.renderstreaming@3.1/manual/sample-multiplay.html">Multiplay sample</a> document page.
</p>
<div id="message"></div>
<section>
<a href="https://github.com/Unity-Technologies/UnityRenderStreaming/tree/develop/WebApp/client/public/multiplay"
title="View source for this page on GitHub" id="viewSource">View source on GitHub</a>
</section>
</div>
<script type="text/javascript" src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="https://unpkg.com/event-target@latest/min.js"></script>
<script src="https://unpkg.com/resize-observer-polyfill@1.5.0/dist/ResizeObserver.global.js"></script>
<script type="module" src="js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,203 @@
import { getServerConfig, getRTCConfiguration } from "../../js/config.js";
import { createDisplayStringArray } from "../../js/stats.js";
import { VideoPlayer } from "../../js/videoplayer.js";
import { RenderStreaming } from "../../module/renderstreaming.js";
import { Signaling, WebSocketSignaling } from "../../module/signaling.js";
/** @enum {number} */
const ActionType = {
ChangeLabel: 0
};
/** @type {Element} */
let playButton;
/** @type {RenderStreaming} */
let renderstreaming;
/** @type {boolean} */
let useWebSocket;
/** @type {RTCDataChannel} */
let multiplayChannel;
const codecPreferences = document.getElementById('codecPreferences');
const supportsSetCodecPreferences = window.RTCRtpTransceiver &&
'setCodecPreferences' in window.RTCRtpTransceiver.prototype;
const messageDiv = document.getElementById('message');
messageDiv.style.display = 'none';
const playerDiv = document.getElementById('player');
const lockMouseCheck = document.getElementById('lockMouseCheck');
const videoPlayer = new VideoPlayer();
setup();
window.document.oncontextmenu = function () {
return false; // cancel default menu
};
window.addEventListener('resize', function () {
videoPlayer.resizeVideo();
}, true);
window.addEventListener('beforeunload', async () => {
if(!renderstreaming)
return;
await renderstreaming.stop();
}, true);
async function setup() {
const res = await getServerConfig();
useWebSocket = res.useWebSocket;
showWarningIfNeeded(res.startupMode);
showCodecSelect();
showPlayButton();
}
function showWarningIfNeeded(startupMode) {
const warningDiv = document.getElementById("warning");
if (startupMode == "private") {
warningDiv.innerHTML = "<h4>Warning</h4> This sample is not working on Private Mode.";
warningDiv.hidden = false;
}
}
function showPlayButton() {
if (!document.getElementById('playButton')) {
const elementPlayButton = document.createElement('img');
elementPlayButton.id = 'playButton';
elementPlayButton.src = '../../images/Play.png';
elementPlayButton.alt = 'Start Streaming';
playButton = document.getElementById('player').appendChild(elementPlayButton);
playButton.addEventListener('click', onClickPlayButton);
}
}
function onClickPlayButton() {
playButton.style.display = 'none';
// add video player
videoPlayer.createPlayer(playerDiv, lockMouseCheck);
setupRenderStreaming();
}
async function setupRenderStreaming() {
codecPreferences.disabled = true;
const signaling = useWebSocket ? new WebSocketSignaling() : new Signaling();
const config = getRTCConfiguration();
renderstreaming = new RenderStreaming(signaling, config);
renderstreaming.onConnect = onConnect;
renderstreaming.onDisconnect = onDisconnect;
renderstreaming.onTrackEvent = (data) => videoPlayer.addTrack(data.track);
renderstreaming.onGotOffer = setCodecPreferences;
await renderstreaming.start();
await renderstreaming.createConnection();
}
function onConnect() {
const channel = renderstreaming.createDataChannel("input");
videoPlayer.setupInput(channel);
multiplayChannel = renderstreaming.createDataChannel("multiplay");
multiplayChannel.onopen = onOpenMultiplayChannel;
showStatsMessage();
}
async function onOpenMultiplayChannel() {
await new Promise(resolve => setTimeout(resolve, 100));
const num = Math.floor(Math.random() * 100000);
const json = JSON.stringify({ type: ActionType.ChangeLabel, argument: String(num) });
multiplayChannel.send(json);
}
async function onDisconnect(connectionId) {
clearStatsMessage();
messageDiv.style.display = 'block';
messageDiv.innerText = `Disconnect peer on ${connectionId}.`;
await renderstreaming.stop();
renderstreaming = null;
multiplayChannel = null;
videoPlayer.deletePlayer();
if (supportsSetCodecPreferences) {
codecPreferences.disabled = false;
}
showPlayButton();
}
function setCodecPreferences() {
/** @type {RTCRtpCodecCapability[] | null} */
let selectedCodecs = null;
if (supportsSetCodecPreferences) {
const preferredCodec = codecPreferences.options[codecPreferences.selectedIndex];
if (preferredCodec.value !== '') {
const [mimeType, sdpFmtpLine] = preferredCodec.value.split(' ');
const { codecs } = RTCRtpSender.getCapabilities('video');
const selectedCodecIndex = codecs.findIndex(c => c.mimeType === mimeType && c.sdpFmtpLine === sdpFmtpLine);
const selectCodec = codecs[selectedCodecIndex];
selectedCodecs = [selectCodec];
}
}
if (selectedCodecs == null) {
return;
}
const transceivers = renderstreaming.getTransceivers().filter(t => t.receiver.track.kind == "video");
if (transceivers && transceivers.length > 0) {
transceivers.forEach(t => t.setCodecPreferences(selectedCodecs));
}
}
function showCodecSelect() {
if (!supportsSetCodecPreferences) {
messageDiv.style.display = 'block';
messageDiv.innerHTML = `Current Browser does not support <a href="https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpTransceiver/setCodecPreferences">RTCRtpTransceiver.setCodecPreferences</a>.`;
return;
}
const codecs = RTCRtpSender.getCapabilities('video').codecs;
codecs.forEach(codec => {
if (['video/red', 'video/ulpfec', 'video/rtx'].includes(codec.mimeType)) {
return;
}
const option = document.createElement('option');
option.value = (codec.mimeType + ' ' + (codec.sdpFmtpLine || '')).trim();
option.innerText = option.value;
codecPreferences.appendChild(option);
});
codecPreferences.disabled = false;
}
/** @type {RTCStatsReport} */
let lastStats;
/** @type {number} */
let intervalId;
function showStatsMessage() {
intervalId = setInterval(async () => {
if (renderstreaming == null) {
return;
}
const stats = await renderstreaming.getStats();
if (stats == null) {
return;
}
const array = createDisplayStringArray(stats, lastStats);
if (array.length) {
messageDiv.style.display = 'block';
messageDiv.innerHTML = array.join('<br>');
}
lastStats = stats;
}, 1000);
}
function clearStatsMessage() {
if (intervalId) {
clearInterval(intervalId);
}
lastStats = null;
intervalId = null;
messageDiv.style.display = 'none';
messageDiv.innerHTML = '';
}

View File

@@ -0,0 +1,43 @@
body {
margin: 0px;
}
#player {
position: relative;
top: 0;
right: 0;
bottom: 0;
left: 0;
align-items: center;
justify-content: center;
display: flex;
background-color: #323232;
}
#player:before {
content: "";
display: block;
padding-top: 66%;
}
#playButton {
width: 15%;
max-width: 200px;
cursor: pointer;
}
#Video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#fullscreenButton {
position: absolute;
top: 25px;
right: 25px;
width: 32px;
height: 32px;
}

View File

@@ -0,0 +1,53 @@
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="icon" href="../images/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="../css/main.css" />
<link rel="stylesheet" href="css/style.css" />
<title>Receiver Sample</title>
</head>
<body>
<div id="container">
<h1>Receiver Sample</h1>
<div id="warning" hidden="true"></div>
<div id="player"></div>
<div class="box">
<span>Codec preferences:</span>
<select id="codecPreferences" autocomplete="off" disabled>
<option selected value="">Default</option>
</select>
</div>
<div class="box">
<span>Lock Cursor to Player:</span>
<input type="checkbox" id="lockMouseCheck" autocomplete="off" />
</div>
<p>
For more information about sample, see
<a href="https://docs.unity3d.com/Packages/com.unity.renderstreaming@3.1/manual/sample-broadcast.html">Broadcast sample</a> document page.
</p>
<div id="message"></div>
<section>
<a href="https://github.com/Unity-Technologies/UnityRenderStreaming/tree/develop/WebApp/client/public/receiver"
title="View source for this page on GitHub" id="viewSource">View source on GitHub</a>
</section>
</div>
<script type="text/javascript" src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="https://unpkg.com/event-target@latest/min.js"></script>
<script src="https://unpkg.com/resize-observer-polyfill@1.5.0/dist/ResizeObserver.global.js"></script>
<script type="module" src="js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,186 @@
import { getServerConfig, getRTCConfiguration } from "../../js/config.js";
import { createDisplayStringArray } from "../../js/stats.js";
import { VideoPlayer } from "../../js/videoplayer.js";
import { RenderStreaming } from "../../module/renderstreaming.js";
import { Signaling, WebSocketSignaling } from "../../module/signaling.js";
/** @type {Element} */
let playButton;
/** @type {RenderStreaming} */
let renderstreaming;
/** @type {boolean} */
let useWebSocket;
const codecPreferences = document.getElementById('codecPreferences');
const supportsSetCodecPreferences = window.RTCRtpTransceiver &&
'setCodecPreferences' in window.RTCRtpTransceiver.prototype;
const messageDiv = document.getElementById('message');
messageDiv.style.display = 'none';
const playerDiv = document.getElementById('player');
const lockMouseCheck = document.getElementById('lockMouseCheck');
const videoPlayer = new VideoPlayer();
setup();
window.document.oncontextmenu = function () {
return false; // cancel default menu
};
window.addEventListener('resize', function () {
videoPlayer.resizeVideo();
}, true);
window.addEventListener('beforeunload', async () => {
if(!renderstreaming)
return;
await renderstreaming.stop();
}, true);
async function setup() {
const res = await getServerConfig();
useWebSocket = res.useWebSocket;
showWarningIfNeeded(res.startupMode);
showCodecSelect();
showPlayButton();
}
function showWarningIfNeeded(startupMode) {
const warningDiv = document.getElementById("warning");
if (startupMode == "private") {
warningDiv.innerHTML = "<h4>Warning</h4> This sample is not working on Private Mode.";
warningDiv.hidden = false;
}
}
function showPlayButton() {
if (!document.getElementById('playButton')) {
const elementPlayButton = document.createElement('img');
elementPlayButton.id = 'playButton';
elementPlayButton.src = '../../images/Play.png';
elementPlayButton.alt = 'Start Streaming';
playButton = document.getElementById('player').appendChild(elementPlayButton);
playButton.addEventListener('click', onClickPlayButton);
}
}
function onClickPlayButton() {
playButton.style.display = 'none';
// add video player
videoPlayer.createPlayer(playerDiv, lockMouseCheck);
setupRenderStreaming();
}
async function setupRenderStreaming() {
codecPreferences.disabled = true;
const signaling = useWebSocket ? new WebSocketSignaling() : new Signaling();
const config = getRTCConfiguration();
renderstreaming = new RenderStreaming(signaling, config);
renderstreaming.onConnect = onConnect;
renderstreaming.onDisconnect = onDisconnect;
renderstreaming.onTrackEvent = (data) => videoPlayer.addTrack(data.track);
renderstreaming.onGotOffer = setCodecPreferences;
await renderstreaming.start();
await renderstreaming.createConnection();
}
function onConnect() {
const channel = renderstreaming.createDataChannel("input");
videoPlayer.setupInput(channel);
showStatsMessage();
}
async function onDisconnect(connectionId) {
clearStatsMessage();
messageDiv.style.display = 'block';
messageDiv.innerText = `Disconnect peer on ${connectionId}.`;
await renderstreaming.stop();
renderstreaming = null;
videoPlayer.deletePlayer();
if (supportsSetCodecPreferences) {
codecPreferences.disabled = false;
}
showPlayButton();
}
function setCodecPreferences() {
/** @type {RTCRtpCodecCapability[] | null} */
let selectedCodecs = null;
if (supportsSetCodecPreferences) {
const preferredCodec = codecPreferences.options[codecPreferences.selectedIndex];
if (preferredCodec.value !== '') {
const [mimeType, sdpFmtpLine] = preferredCodec.value.split(' ');
const { codecs } = RTCRtpSender.getCapabilities('video');
const selectedCodecIndex = codecs.findIndex(c => c.mimeType === mimeType && c.sdpFmtpLine === sdpFmtpLine);
const selectCodec = codecs[selectedCodecIndex];
selectedCodecs = [selectCodec];
}
}
if (selectedCodecs == null) {
return;
}
const transceivers = renderstreaming.getTransceivers().filter(t => t.receiver.track.kind == "video");
if (transceivers && transceivers.length > 0) {
transceivers.forEach(t => t.setCodecPreferences(selectedCodecs));
}
}
function showCodecSelect() {
if (!supportsSetCodecPreferences) {
messageDiv.style.display = 'block';
messageDiv.innerHTML = `Current Browser does not support <a href="https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpTransceiver/setCodecPreferences">RTCRtpTransceiver.setCodecPreferences</a>.`;
return;
}
const codecs = RTCRtpSender.getCapabilities('video').codecs;
codecs.forEach(codec => {
if (['video/red', 'video/ulpfec', 'video/rtx'].includes(codec.mimeType)) {
return;
}
const option = document.createElement('option');
option.value = (codec.mimeType + ' ' + (codec.sdpFmtpLine || '')).trim();
option.innerText = option.value;
codecPreferences.appendChild(option);
});
codecPreferences.disabled = false;
}
/** @type {RTCStatsReport} */
let lastStats;
/** @type {number} */
let intervalId;
function showStatsMessage() {
intervalId = setInterval(async () => {
if (renderstreaming == null) {
return;
}
const stats = await renderstreaming.getStats();
if (stats == null) {
return;
}
const array = createDisplayStringArray(stats, lastStats);
if (array.length) {
messageDiv.style.display = 'block';
messageDiv.innerHTML = array.join('<br>');
}
lastStats = stats;
}, 1000);
}
function clearStatsMessage() {
if (intervalId) {
clearInterval(intervalId);
}
lastStats = null;
intervalId = null;
messageDiv.style.display = 'none';
messageDiv.innerHTML = '';
}

View File

@@ -0,0 +1,103 @@
body {
margin: 0px;
}
button#muteButton {
margin: 5px 0;
width: auto;
}
#player {
position: relative;
top: 0;
right: 0;
bottom: 0;
left: 0;
align-items: center;
justify-content: center;
display: flex;
background-color: #323232;
}
#player:before {
content: "";
display: block;
padding-top: 66%;
}
#playButton {
width: 15%;
max-width: 200px;
cursor: pointer;
}
#Video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#VideoThumbnail {
position: absolute;
top: 0;
left: 0;
width: 30%;
height: 30%;
}
#greenButton {
position: absolute;
bottom: 10px;
left: 10px;
width: 160px;
background-color: #4CAF50;
/* Green */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
#blueButton {
position: absolute;
bottom: 10px;
left: 180px;
width: 160px;
background-color: #447FAF;
/* Blue */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
#orangeButton {
position: absolute;
bottom: 10px;
left: 350px;
width: 160px;
background-color: #FF7700;
/* Blue */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
#fullscreenButton {
position: absolute;
top: 25px;
right: 25px;
width: 32px;
height: 32px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

View File

@@ -0,0 +1,37 @@
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="icon" href="../images/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="../css/main.css" />
<link rel="stylesheet" href="css/style.css" />
<title>VideoPlayer Sample</title>
</head>
<body>
<div id="container">
<h1>VideoPlayer Sample</h1>
<div id="warning" hidden=true></div>
<div id="player"></div>
<p>For more information about <code>WebBrowserInput</code> sample, see <a href="https://docs.unity3d.com/Packages/com.unity.renderstreaming@latest/sample-browserinput.html">WebBrowserInput
sample</a> document page.</p>
<section>
<a href="https://github.com/Unity-Technologies/UnityRenderStreaming/tree/develop/WebApp/public/videoplayer"
title="View source for this page on GitHub" id="viewSource">View source on GitHub</a>
</section>
</div>
<script type="text/javascript" src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="https://unpkg.com/event-target@latest/min.js"></script>
<script src="https://unpkg.com/resize-observer-polyfill@1.5.0/dist/ResizeObserver.global.js"></script>
<script type="module" src="js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,146 @@
import * as Logger from "../../module/logger.js";
const _e = 0.09;
const _gameloopInterval = 16.67; //in milliseconds, 60 times a second
var gameloop = null;
var gamepadsPreviousButtonsStates = {};
var gamepadsPreviousAxesStates = {};
var gamepadsConnectedTimeStamp = {};
const _axisOffset = 100;
const _axisMultiplier = 1;
const _axisYInverted = -1;
class GamepadButtonEvent extends Event {
constructor() {
super(...arguments);
this.index = arguments[1].index;
this.id = arguments[1].id;
this.value = arguments[1].value;
}
}
class GamepadAxisEvent extends Event {
constructor() {
super(...arguments);
this.index = arguments[1].index;
this.x = arguments[1].x;
this.y = arguments[1].y;
this.id = arguments[1].id;
}
}
function storePreviousState(gamepad) {
gamepadsPreviousButtonsStates[gamepad.index] = {};
gamepad.buttons.forEach(function (button, index) {
gamepadsPreviousButtonsStates[gamepad.index][index] = { value: button.value, pressed: button.pressed };
});
gamepadsPreviousAxesStates[gamepad.index] = [gamepad.axes.length];
for (var index = 0; index < gamepad.axes.length; index++)
gamepadsPreviousAxesStates[gamepad.index][index] = gamepad.axes[index];
}
function checkAxes(gamepad, previousGamePad) {
for (var i = 0; i < gamepad.axes.length; i += 2) {
var absX = Math.abs(gamepad.axes[i]);
var absY = Math.abs(gamepad.axes[i + 1]);
var event = null;
if ((absX > _e) ||
(absY > _e)) {
event = new GamepadAxisEvent('gamepadAxis', { id: gamepadsConnectedTimeStamp[gamepad.index], index: i / 2 + _axisOffset, x: gamepad.axes[i] * _axisMultiplier, y: gamepad.axes[i + 1] * _axisMultiplier * _axisYInverted });
document.dispatchEvent(event);
}
else {
var previousAbsX = Math.abs(previousGamePad[i]);
var previousAbsY = Math.abs(previousGamePad[i + 1]);
//have to send if previously was moved
if ((previousAbsX > _e) ||
(previousAbsY > _e)) {
event = new GamepadAxisEvent('gamepadAxis', { id: gamepadsConnectedTimeStamp[gamepad.index], index: i / 2 + _axisOffset, x: 0.0, y: 0.0 });
document.dispatchEvent(event);
}
}
}
}
function gameLoop() {
Object.keys(gamepadsPreviousAxesStates).forEach(function (gamepadIndex) {
var gamepad = navigator.webkitGetGamepads ? navigator.webkitGetGamepads()[gamepadIndex] : navigator.getGamepads()[gamepadIndex];
var previousButtons = gamepadsPreviousButtonsStates[gamepadIndex];
gamepad.buttons.forEach(function (button, index) {
var buttonStatus = navigator.webkitGetGamepads ? button == 1 : (button.value > 0 || button.pressed == true);
var previousButtonStatus = navigator.webkitGetGamepads ? previousButtons[index].value == 1 : (previousButtons[index].value > 0 || previousButtons[index].pressed == true);
var event;
if (buttonStatus != previousButtonStatus) {
if (buttonStatus) {
event = new GamepadButtonEvent('gamepadButtonDown', { id: gamepadsConnectedTimeStamp[gamepad.index], index: index, value: button.value });
}
else {
event = new GamepadButtonEvent('gamepadButtonUp', { id: gamepadsConnectedTimeStamp[gamepad.index], index: index, value: 0 });
}
document.dispatchEvent(event);
}
else if (buttonStatus) {
event = new GamepadButtonEvent('gamepadButtonPressed', { id: gamepadsConnectedTimeStamp[gamepad.index], index: index, value: button.value });
document.dispatchEvent(event);
}
});
checkAxes(gamepad, gamepadsPreviousAxesStates[gamepadIndex]);
storePreviousState(gamepad);
});
}
function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
export function gamepadHandler(event, connecting) {
var gamepad = event.gamepad;
var key = gamepad.id.replace(/\s/g, '');
var cookieTimeStamp = getCookie(key);
if (connecting) {
storePreviousState(gamepad);
if (Object.keys(gamepadsPreviousAxesStates).length == 1) {
gameloop = setInterval(gameLoop, _gameloopInterval);
}
//try to find the timestamp
//need to strip the : from the id
if (cookieTimeStamp == "") {
document.cookie = key + "=" + gamepad.timestamp;
gamepadsConnectedTimeStamp[gamepad.index] = gamepad.timestamp;
}
else {
gamepadsConnectedTimeStamp[gamepad.index] = cookieTimeStamp;
}
Logger.log("connected: " + gamepadsConnectedTimeStamp[gamepad.index]);
} else {
delete gamepadsPreviousAxesStates[gamepad.index];
delete gamepadsPreviousButtonsStates[gamepad.index];
if (Object.keys(gamepadsPreviousAxesStates).length == 0) {
clearInterval(gameloop);
gameloop = null;
}
Logger.log("disconnected: " + gamepad.id);
}
}

View File

@@ -0,0 +1,156 @@
import { VideoPlayer } from "./video-player.js";
import { registerGamepadEvents, registerKeyboardEvents, registerMouseEvents, sendClickEvent } from "./register-events.js";
import { getServerConfig } from "../../js/config.js";
setup();
let playButton;
let videoPlayer;
let useWebSocket;
window.document.oncontextmenu = function () {
return false; // cancel default menu
};
window.addEventListener('resize', function () {
videoPlayer.resizeVideo();
}, true);
window.addEventListener('beforeunload', async () => {
await videoPlayer.stop();
}, true);
async function setup() {
const res = await getServerConfig();
useWebSocket = res.useWebSocket;
showWarningIfNeeded(res.startupMode);
showPlayButton();
}
function showWarningIfNeeded(startupMode) {
const warningDiv = document.getElementById("warning");
if (startupMode == "private") {
warningDiv.innerHTML = "<h4>Warning</h4> This sample is not working on Private Mode.";
warningDiv.hidden = false;
}
}
function showPlayButton() {
if (!document.getElementById('playButton')) {
let elementPlayButton = document.createElement('img');
elementPlayButton.id = 'playButton';
elementPlayButton.src = 'images/Play.png';
elementPlayButton.alt = 'Start Streaming';
playButton = document.getElementById('player').appendChild(elementPlayButton);
playButton.addEventListener('click', onClickPlayButton);
}
}
function onClickPlayButton() {
playButton.style.display = 'none';
const playerDiv = document.getElementById('player');
// add video player
const elementVideo = document.createElement('video');
elementVideo.id = 'Video';
elementVideo.style.touchAction = 'none';
playerDiv.appendChild(elementVideo);
// add video thumbnail
const elementVideoThumb = document.createElement('video');
elementVideoThumb.id = 'VideoThumbnail';
elementVideoThumb.style.touchAction = 'none';
playerDiv.appendChild(elementVideoThumb);
setupVideoPlayer([elementVideo, elementVideoThumb]).then(value => videoPlayer = value);
// add blue button
const elementBlueButton = document.createElement('button');
elementBlueButton.id = "blueButton";
elementBlueButton.innerHTML = "Light on";
playerDiv.appendChild(elementBlueButton);
elementBlueButton.addEventListener("click", function () {
sendClickEvent(videoPlayer, 1);
});
// add green button
const elementGreenButton = document.createElement('button');
elementGreenButton.id = "greenButton";
elementGreenButton.innerHTML = "Light off";
playerDiv.appendChild(elementGreenButton);
elementGreenButton.addEventListener("click", function () {
sendClickEvent(videoPlayer, 2);
});
// add orange button
const elementOrangeButton = document.createElement('button');
elementOrangeButton.id = "orangeButton";
elementOrangeButton.innerHTML = "Play audio";
playerDiv.appendChild(elementOrangeButton);
elementOrangeButton.addEventListener("click", function () {
sendClickEvent(videoPlayer, 3);
});
// add fullscreen button
const elementFullscreenButton = document.createElement('img');
elementFullscreenButton.id = 'fullscreenButton';
elementFullscreenButton.src = 'images/FullScreen.png';
playerDiv.appendChild(elementFullscreenButton);
elementFullscreenButton.addEventListener("click", function () {
if (!document.fullscreenElement || !document.webkitFullscreenElement) {
if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen();
}
else if (document.documentElement.webkitRequestFullscreen) {
document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
} else {
if (playerDiv.style.position == "absolute") {
playerDiv.style.position = "relative";
} else {
playerDiv.style.position = "absolute";
}
}
}
});
document.addEventListener('webkitfullscreenchange', onFullscreenChange);
document.addEventListener('fullscreenchange', onFullscreenChange);
function onFullscreenChange() {
if (document.webkitFullscreenElement || document.fullscreenElement) {
playerDiv.style.position = "absolute";
elementFullscreenButton.style.display = 'none';
}
else {
playerDiv.style.position = "relative";
elementFullscreenButton.style.display = 'block';
}
}
}
async function setupVideoPlayer(elements) {
const videoPlayer = new VideoPlayer(elements);
await videoPlayer.setupConnection(useWebSocket);
videoPlayer.ondisconnect = onDisconnect;
registerGamepadEvents(videoPlayer);
registerKeyboardEvents(videoPlayer);
registerMouseEvents(videoPlayer, elements[0]);
return videoPlayer;
}
function onDisconnect() {
const playerDiv = document.getElementById('player');
clearChildren(playerDiv);
videoPlayer.stop();
videoPlayer = null;
showPlayButton();
}
function clearChildren(element) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}

View File

@@ -0,0 +1,307 @@
import { gamepadHandler } from "./gamepadEvents.js";
import * as Logger from "../../module/logger.js";
import { Keymap } from "../../module/keymap.js";
const InputEvent = {
Keyboard: 0,
Mouse: 1,
MouseWheel: 2,
Touch: 3,
ButtonClick: 4,
Gamepad: 5
};
const KeyboardEventType = {
Up: 0,
Down: 1
};
const GamepadEventType = {
ButtonUp: 0,
ButtonDown: 1,
ButtonPressed: 2,
Axis: 3
};
const PointerPhase = {
None: 0,
Began: 1,
Moved: 2,
Ended: 3,
Canceled: 4,
Stationary: 5
};
let sendGamepadButtonDown = undefined;
let sendGamepadButtonUp = undefined;
let sendGamepadButtonPressed;
let gamepadAxisChange = undefined;
let gamepadConnected = undefined;
let gamepadDisconnected = undefined;
export function registerGamepadEvents(videoPlayer) {
const _videoPlayer = videoPlayer;
sendGamepadButtonDown = (e) => {
Logger.log("gamepad id: " + e.id + " button index: " + e.index + " value " + e.value + " down");
let data = new DataView(new ArrayBuffer(19));
data.setUint8(0, InputEvent.Gamepad);
data.setUint8(1, GamepadEventType.ButtonDown);
data.setUint8(2, e.index);
data.setFloat64(3, e.value, true);
_videoPlayer && _videoPlayer.sendMsg(data.buffer);
};
sendGamepadButtonUp = (e) => {
Logger.log("gamepad id: " + e.id + " button index: " + e.index + " value " + e.value + " up");
let data = new DataView(new ArrayBuffer(19));
data.setUint8(0, InputEvent.Gamepad);
data.setUint8(1, GamepadEventType.ButtonUp);
data.setUint8(2, e.index);
data.setFloat64(3, e.value, true);
_videoPlayer && _videoPlayer.sendMsg(data.buffer);
};
sendGamepadButtonPressed = (e) => {
Logger.log("gamepad id: " + e.id + " button index: " + e.index + " value " + e.value + " pressed");
let data = new DataView(new ArrayBuffer(19));
data.setUint8(0, InputEvent.Gamepad);
data.setUint8(1, GamepadEventType.ButtonPressed);
data.setUint8(2, e.index);
data.setFloat64(3, e.value, true);
_videoPlayer && _videoPlayer.sendMsg(data.buffer);
};
gamepadAxisChange = (e) => {
Logger.log("gamepad id: " + e.id + " axis: " + e.index + " value " + e.value + " x:" + e.x + " y:" + e.y);
let data = new DataView(new ArrayBuffer(27));
data.setUint8(0, InputEvent.Gamepad);
data.setUint8(1, GamepadEventType.Axis);
data.setUint8(2, e.index);
data.setFloat64(3, e.x, true);
data.setFloat64(11, e.y, true);
_videoPlayer && _videoPlayer.sendMsg(data.buffer);
};
gamepadConnected = (e) => { gamepadHandler(e, true); };
gamepadDisconnected = (e) => { gamepadHandler(e, false); };
document.addEventListener("gamepadButtonDown", sendGamepadButtonDown, false);
document.addEventListener("gamepadButtonUp", sendGamepadButtonUp, false);
document.addEventListener("gamepadButtonPressed", sendGamepadButtonPressed, false);
document.addEventListener("gamepadAxis", gamepadAxisChange, false);
window.addEventListener("gamepadconnected", gamepadConnected, false);
window.addEventListener("gamepaddisconnected", gamepadDisconnected, false);
}
export function unregisterGamepadEvents() {
document.removeEventListener("gamepadButtonDown", sendGamepadButtonDown, false);
document.removeEventListener("gamepadButtonUp", sendGamepadButtonUp, false);
document.removeEventListener("gamepadButtonPressed", sendGamepadButtonPressed, false);
document.removeEventListener("gamepadAxis", gamepadAxisChange, false);
window.removeEventListener("gamepadconnected", gamepadConnected, false);
window.removeEventListener("gamepaddisconnected", gamepadDisconnected, false);
}
let sendKeyUp = undefined;
let sendKeyDown = undefined;
export function registerKeyboardEvents(videoPlayer) {
const _videoPlayer = videoPlayer;
function sendKey(e, type) {
const key = Keymap[e.code];
const character = e.key.length === 1 ? e.key.charCodeAt(0) : 0;
Logger.log("key down " + key + ", repeat = " + e.repeat + ", character = " + character);
_videoPlayer && _videoPlayer.sendMsg(new Uint8Array([InputEvent.Keyboard, type, e.repeat, key, character]).buffer);
}
sendKeyUp = (e) => {
sendKey(e, KeyboardEventType.Up);
};
sendKeyDown = (e) => {
sendKey(e, KeyboardEventType.Down);
};
document.addEventListener('keyup', sendKeyUp, false);
document.addEventListener('keydown', sendKeyDown, false);
}
export function unregisterKeyboardEvents() {
//Stop listening to keyboard events
document.removeEventListener('keyup', sendKeyUp, false);
document.removeEventListener('keydown', sendKeyDown, false);
}
let sendMouse = undefined;
let sendMouseWheel = undefined;
let sendTouchEnd = undefined;
let sendTouchStart = undefined;
let sendTouchCancel = undefined;
let sendTouchMove = undefined;
export function registerMouseEvents(videoPlayer, playerElement) {
const _videoPlayer = videoPlayer;
function sendTouch(e, phase) {
const changedTouches = Array.from(e.changedTouches);
const touches = Array.from(e.touches);
const phrases = [];
for (let i = 0; i < changedTouches.length; i++) {
if (touches.find(function (t) {
return t.identifier === changedTouches[i].identifier;
}) === undefined) {
touches.push(changedTouches[i]);
}
}
for (let i = 0; i < touches.length; i++) {
touches[i].identifier;
phrases[i] = changedTouches.find(
function (e) {
return e.identifier === touches[i].identifier;
}) === undefined ? PointerPhase.Stationary : phase;
}
Logger.log("touch phase:" + phase + " length:" + changedTouches.length + " pageX" + changedTouches[0].pageX + ", pageX: " + changedTouches[0].pageY + ", force:" + changedTouches[0].force);
let data = new DataView(new ArrayBuffer(2 + 13 * touches.length));
data.setUint8(0, InputEvent.Touch);
data.setUint8(1, touches.length);
let byteOffset = 2;
for (let i = 0; i < touches.length; i++) {
const scale = _videoPlayer.videoScale;
const originX = _videoPlayer.videoOriginX;
const originY = _videoPlayer.videoOriginY;
const x = (touches[i].pageX - originX) / scale;
// According to Unity Coordinate system
// const y = (touches[i].pageX - originY) / scale;
const y = _videoPlayer.videoHeight - (touches[i].pageY - originY) / scale;
data.setInt32(byteOffset, touches[i].identifier, true);
byteOffset += 4;
data.setUint8(byteOffset, phrases[i]);
byteOffset += 1;
data.setInt16(byteOffset, x, true);
byteOffset += 2;
data.setInt16(byteOffset, y, true);
byteOffset += 2;
data.setFloat32(byteOffset, touches[i].force, true);
byteOffset += 4;
}
_videoPlayer && _videoPlayer.sendMsg(data.buffer);
}
sendTouchMove = (e) => {
sendTouch(e, PointerPhase.Moved);
e.preventDefault();
};
sendTouchStart = (e) => {
sendTouch(e, PointerPhase.Began);
e.preventDefault();
};
sendTouchEnd = (e) => {
sendTouch(e, PointerPhase.Ended);
e.preventDefault();
};
sendTouchCancel = (e) => {
sendTouch(e, PointerPhase.Canceled);
e.preventDefault();
};
sendMouse = (e) => {
const scale = _videoPlayer.videoScale;
const originX = _videoPlayer.videoOriginX;
const originY = _videoPlayer.videoOriginY;
const x = (e.clientX - originX) / scale;
// According to Unity Coordinate system
// const y = (e.clientY - originY) / scale;
const y = _videoPlayer.videoHeight - (e.clientY - originY) / scale;
Logger.log("x: " + x + ", y: " + y + ", scale: " + scale + ", originX: " + originX + ", originY: " + originY + " mouse button:" + e.buttons);
let data = new DataView(new ArrayBuffer(6));
data.setUint8(0, InputEvent.Mouse);
data.setInt16(1, x, true);
data.setInt16(3, y, true);
data.setUint8(5, e.buttons);
_videoPlayer && _videoPlayer.sendMsg(data.buffer);
};
function sendMouseWheel(e) {
Logger.log("mouse wheel with delta " + e.wheelDelta);
let data = new DataView(new ArrayBuffer(9));
data.setUint8(0, InputEvent.MouseWheel);
data.setFloat32(1, e.deltaX, true);
data.setFloat32(5, e.deltaY, true);
_videoPlayer && _videoPlayer.sendMsg(data.buffer);
}
// Listen to mouse events
playerElement.addEventListener('click', sendMouse, false);
playerElement.addEventListener('mousedown', sendMouse, false);
playerElement.addEventListener('mouseup', sendMouse, false);
playerElement.addEventListener('mousemove', sendMouse, false);
playerElement.addEventListener('wheel', sendMouseWheel, false);
// Listen to touch events based on "Touch Events Level1" TR.
//
// Touch event Level1 https://www.w3.org/TR/touch-events/
// Touch event Level2 https://w3c.github.io/touch-events/
//
playerElement.addEventListener('touchend', sendTouchEnd, false);
playerElement.addEventListener('touchstart', sendTouchStart, false);
playerElement.addEventListener('touchcancel', sendTouchCancel, false);
playerElement.addEventListener('touchmove', sendTouchMove, false);
}
export function unregisterMouseEvents(playerElement) {
// Stop listening to mouse events
playerElement.removeEventListener('click', sendMouse, false);
playerElement.removeEventListener('mousedown', sendMouse, false);
playerElement.removeEventListener('mouseup', sendMouse, false);
playerElement.removeEventListener('mousemove', sendMouse, false);
playerElement.removeEventListener('wheel', sendMouseWheel, false);
// Stop listening to touch events based on "Touch Events Level1" TR.
playerElement.removeEventListener('touchend', sendTouchEnd, false);
playerElement.removeEventListener('touchstart', sendTouchStart, false);
playerElement.removeEventListener('touchcancel', sendTouchCancel, false);
playerElement.removeEventListener('touchmove', sendTouchMove, false);
}
export function sendClickEvent(videoPlayer, elementId) {
let data = new DataView(new ArrayBuffer(3));
data.setUint8(0, InputEvent.ButtonClick);
data.setInt16(1, elementId, true);
videoPlayer && videoPlayer.sendMsg(data.buffer);
}

View File

@@ -0,0 +1,246 @@
import { Signaling, WebSocketSignaling } from "../../module/signaling.js";
import Peer from "../../module/peer.js";
import * as Logger from "../../module/logger.js";
// enum type of event sending from Unity
var UnityEventType = {
SWITCH_VIDEO: 0
};
function uuid4() {
var temp_url = URL.createObjectURL(new Blob());
var uuid = temp_url.toString();
URL.revokeObjectURL(temp_url);
return uuid.split(/[:/]/g).pop().toLowerCase(); // remove prefixes
}
export class VideoPlayer {
constructor(elements) {
const _this = this;
this.pc = null;
this.channel = null;
this.connectionId = null;
// main video
this.localStream = new MediaStream();
this.video = elements[0];
this.video.playsInline = true;
this.video.addEventListener('loadedmetadata', function () {
_this.video.play();
_this.resizeVideo();
}, true);
// secondly video
this.localStream2 = new MediaStream();
this.videoThumb = elements[1];
this.videoThumb.playsInline = true;
this.videoThumb.addEventListener('loadedmetadata', function () {
_this.videoThumb.play();
}, true);
this.videoTrackList = [];
this.videoTrackIndex = 0;
this.maxVideoTrackLength = 2;
this.ondisconnect = function () { };
}
async setupConnection(useWebSocket) {
const _this = this;
// close current RTCPeerConnection
if (this.pc) {
Logger.log('Close current PeerConnection');
this.pc.close();
this.pc = null;
}
if (useWebSocket) {
this.signaling = new WebSocketSignaling();
} else {
this.signaling = new Signaling();
}
this.connectionId = uuid4();
// Create peerConnection with proxy server and set up handlers
this.pc = new Peer(this.connectionId, true);
this.pc.addEventListener('disconnect', () => {
_this.ondisconnect();
});
this.pc.addEventListener('trackevent', (e) => {
const data = e.detail;
if (data.track.kind == 'video') {
_this.videoTrackList.push(data.track);
}
if (data.track.kind == 'audio') {
_this.localStream.addTrack(data.track);
}
if (_this.videoTrackList.length == _this.maxVideoTrackLength) {
_this.switchVideo(_this.videoTrackIndex);
}
});
this.pc.addEventListener('sendoffer', (e) => {
const offer = e.detail;
_this.signaling.sendOffer(offer.connectionId, offer.sdp);
});
this.pc.addEventListener('sendanswer', (e) => {
const answer = e.detail;
_this.signaling.sendAnswer(answer.connectionId, answer.sdp);
});
this.pc.addEventListener('sendcandidate', (e) => {
const candidate = e.detail;
_this.signaling.sendCandidate(candidate.connectionId, candidate.candidate, candidate.sdpMid, candidate.sdpMLineIndex);
});
this.signaling.addEventListener('disconnect', async (e) => {
const data = e.detail;
if (_this.pc != null && _this.pc.connectionId == data.connectionId) {
_this.ondisconnect();
}
});
this.signaling.addEventListener('offer', async (e) => {
const offer = e.detail;
const desc = new RTCSessionDescription({ sdp: offer.sdp, type: "offer" });
if (_this.pc != null) {
await _this.pc.onGotDescription(offer.connectionId, desc);
}
});
this.signaling.addEventListener('answer', async (e) => {
const answer = e.detail;
const desc = new RTCSessionDescription({ sdp: answer.sdp, type: "answer" });
if (_this.pc != null) {
await _this.pc.onGotDescription(answer.connectionId, desc);
}
});
this.signaling.addEventListener('candidate', async (e) => {
const candidate = e.detail;
const iceCandidate = new RTCIceCandidate({ candidate: candidate.candidate, sdpMid: candidate.sdpMid, sdpMLineIndex: candidate.sdpMLineIndex });
if (_this.pc != null) {
await _this.pc.onGotCandidate(candidate.connectionId, iceCandidate);
}
});
// setup signaling
await this.signaling.start();
// Create data channel with proxy server and set up handlers
this.channel = this.pc.createDataChannel(this.connectionId, 'data');
this.channel.onopen = function () {
Logger.log('Datachannel connected.');
};
this.channel.onerror = function (e) {
Logger.log("The error " + e.error.message + " occurred\n while handling data with proxy server.");
};
this.channel.onclose = function () {
Logger.log('Datachannel disconnected.');
};
this.channel.onmessage = async (msg) => {
// receive message from unity and operate message
let data;
// receive message data type is blob only on Firefox
if (navigator.userAgent.indexOf('Firefox') != -1) {
data = await msg.data.arrayBuffer();
} else {
data = msg.data;
}
const bytes = new Uint8Array(data);
_this.videoTrackIndex = bytes[1];
switch (bytes[0]) {
case UnityEventType.SWITCH_VIDEO:
_this.switchVideo(_this.videoTrackIndex);
break;
}
};
}
resizeVideo() {
const clientRect = this.video.getBoundingClientRect();
const videoRatio = this.videoWidth / this.videoHeight;
const clientRatio = clientRect.width / clientRect.height;
this._videoScale = videoRatio > clientRatio ? clientRect.width / this.videoWidth : clientRect.height / this.videoHeight;
const videoOffsetX = videoRatio > clientRatio ? 0 : (clientRect.width - this.videoWidth * this._videoScale) * 0.5;
const videoOffsetY = videoRatio > clientRatio ? (clientRect.height - this.videoHeight * this._videoScale) * 0.5 : 0;
this._videoOriginX = clientRect.left + videoOffsetX;
this._videoOriginY = clientRect.top + videoOffsetY;
}
// switch streaming destination main video and secondly video
switchVideo(indexVideoTrack) {
this.video.srcObject = this.localStream;
this.videoThumb.srcObject = this.localStream2;
if (indexVideoTrack == 0) {
this.replaceTrack(this.localStream, this.videoTrackList[0]);
this.replaceTrack(this.localStream2, this.videoTrackList[1]);
}
else {
this.replaceTrack(this.localStream, this.videoTrackList[1]);
this.replaceTrack(this.localStream2, this.videoTrackList[0]);
}
}
// replace video track related the MediaStream
replaceTrack(stream, newTrack) {
const tracks = stream.getVideoTracks();
for (const track of tracks) {
if (track.kind == 'video') {
stream.removeTrack(track);
}
}
stream.addTrack(newTrack);
}
get videoWidth() {
return this.video.videoWidth;
}
get videoHeight() {
return this.video.videoHeight;
}
get videoOriginX() {
return this._videoOriginX;
}
get videoOriginY() {
return this._videoOriginY;
}
get videoScale() {
return this._videoScale;
}
sendMsg(msg) {
if (this.channel == null) {
return;
}
switch (this.channel.readyState) {
case 'connecting':
Logger.log('Connection not ready');
break;
case 'open':
this.channel.send(msg);
break;
case 'closing':
Logger.log('Attempt to sendMsg message while closing');
break;
case 'closed':
Logger.log('Attempt to sendMsg message while connection closed.');
break;
}
}
async stop() {
if (this.signaling) {
await this.signaling.stop();
this.signaling = null;
}
if (this.pc) {
this.pc.close();
this.pc = null;
}
}
}