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,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();
}
}