UNPKG

ovenlivekit

Version:

OvenLiveKit for Web is an open source JavaScript SDK suite for live streaming from web browsers to OvenMediaEngine.

79 lines (75 loc) 40.2 kB
/* * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development"). * This devtool is neither made for production nor for readable output files. * It uses "eval()" calls to create a separate source file in the browser devtools. * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/) * or disable the default devtool with "devtool: false". * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/). */ (function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["OvenLiveKit"] = factory(); else root["OvenLiveKit"] = factory(); })(self, () => { return /******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ var __webpack_modules__ = ({ /***/ "./src/OvenLiveKit.js": /*!****************************!*\ !*** ./src/OvenLiveKit.js ***! \****************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\nconst OvenLiveKit = {};\n\nconst version = '1.5.8';\nconst logHeader = 'OvenLiveKit.js :';\nconst logEventHeader = 'OvenLiveKit.js ====';\n\n// private methods\nfunction getTransportQuery(endpointUrl) {\n\n try {\n const url = new URL(endpointUrl);\n const transport = url.searchParams.get('transport');\n\n if (transport === null) {\n return undefined;\n }\n\n const normalized = transport.toLowerCase();\n\n return normalized;\n } catch (e) {\n return undefined;\n }\n}\n\nfunction sendMessage(webSocket, message) {\n\n if (webSocket) {\n webSocket.send(JSON.stringify(message));\n }\n}\n\nfunction generateDomainFromUrl(url) {\n let result = '';\n let match;\n if (match = url.match(/^(?:wss?:\\/\\/)?(?:[^@\\n]+@)?(?:www\\.)?([^:\\/\\n\\?\\=]+)/im)) {\n result = match[1];\n }\n\n return result;\n}\n\nfunction findIp(string) {\n\n let result = '';\n let match;\n\n if (match = string.match(new RegExp('\\\\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\b', 'gi'))) {\n result = match[0];\n }\n\n return result;\n}\n\nfunction getFormatNumber(sdp, format) {\n\n const lines = sdp.split('\\r\\n');\n let formatNumber = -1;\n\n for (let i = 0; i < lines.length - 1; i++) {\n\n lines[i] = lines[i].toLowerCase();\n\n if (lines[i].indexOf('a=rtpmap') === 0 && lines[i].indexOf(format.toLowerCase()) > -1) {\n // parsing \"a=rtpmap:100 H264/90000\" line\n // a=rtpmap:<payload type> <encoding name>/<clock rate>[/<encoding parameters >]\n formatNumber = lines[i].split(' ')[0].split(':')[1];\n break;\n }\n }\n\n return formatNumber;\n}\n\nfunction setPreferredVideoFormat(sdp, formatName) {\n\n const formatNumber = getFormatNumber(sdp, formatName);\n\n if (formatNumber === -1) {\n return sdp;\n }\n\n let newLines = [];\n const lines = sdp.split('\\r\\n');\n\n for (let i = 0; i < lines.length - 1; i++) {\n\n const line = lines[i];\n\n if (line.indexOf('m=video') === 0) {\n\n // m=<media> <port>/<number of ports> <transport> <fmt list>\n const others = line.split(' ').slice(0, 3);\n const formats = line.split(' ').slice(3);\n formats.sort(function (x, y) { return x == formatNumber ? -1 : y == formatNumber ? 1 : 0; });\n newLines.push(others.concat(formats).join(' '));\n } else {\n newLines.push(line);\n }\n\n }\n\n return newLines.join('\\r\\n') + '\\r\\n';\n}\n\nfunction removeFormat(sdp, formatNumber) {\n let newLines = [];\n let lines = sdp.split('\\r\\n');\n\n for (let i = 0; i < lines.length; i++) {\n\n if (lines[i].indexOf('m=video') === 0) {\n newLines.push(lines[i].replace(' ' + formatNumber + '', ''));\n } else if (lines[i].indexOf(formatNumber + '') > -1) {\n\n } else {\n newLines.push(lines[i]);\n }\n }\n\n return newLines.join('\\r\\n')\n}\n\nasync function getStreamForDeviceCheck(type) {\n\n // High resolution video constraints makes browser to get maximum resolution of video device.\n const constraints = {\n };\n\n if (type === 'both') {\n constraints.audio = true;\n constraints.video = true;\n } else if (type === 'audio') {\n constraints.audio = true;\n } else if (type === 'video') {\n constraints.video = true;\n }\n\n const permissionStream = await navigator.mediaDevices.getUserMedia(constraints);\n permissionStream.getTracks().forEach(track => track.stop());\n await new Promise(resolve => setTimeout(resolve, 500));\n\n return true;\n}\n\nasync function getDevices() {\n\n return await navigator.mediaDevices.enumerateDevices();\n}\n\nfunction gotDevices(deviceInfos) {\n\n let devices = {\n 'audioinput': [],\n 'audiooutput': [],\n 'videoinput': [],\n 'other': [],\n };\n\n for (let i = 0; i !== deviceInfos.length; ++i) {\n\n const deviceInfo = deviceInfos[i];\n\n let info = {};\n\n info.deviceId = deviceInfo.deviceId;\n\n if (deviceInfo.kind === 'audioinput') {\n\n info.label = deviceInfo.label || `microphone ${devices.audioinput.length + 1}`;\n devices.audioinput.push(info);\n } else if (deviceInfo.kind === 'audiooutput') {\n\n info.label = deviceInfo.label || `speaker ${devices.audiooutput.length + 1}`;\n devices.audiooutput.push(info);\n } else if (deviceInfo.kind === 'videoinput') {\n\n info.label = deviceInfo.label || `camera ${devices.videoinput.length + 1}`;\n devices.videoinput.push(info);\n } else {\n\n info.label = deviceInfo.label || `other ${devices.other.length + 1}`;\n devices.other.push(info);\n }\n }\n\n return devices;\n}\n\nfunction initConfig(instance, options) {\n\n // webrtc or whip\n instance.streamingMode = null;\n\n instance.inputStream = null;\n instance.webSocket = null;\n instance.peerConnection = null;\n instance.connectionConfig = {};\n\n instance.videoElement = null;\n instance.endpointUrl = null;\n instance.resourceUrl = null;\n\n if (options && options.callbacks) {\n\n instance.callbacks = options.callbacks;\n } else {\n instance.callbacks = {};\n }\n}\n\nfunction addMethod(instance) {\n\n function errorHandler(error) {\n\n if (instance.callbacks.error) {\n\n instance.callbacks.error(error);\n }\n }\n\n async function fetchWithRedirect(url, options) {\n let fetched = await fetch(url, options);\n\n while (fetched.redirected) {\n url = fetched.url;\n fetched = await fetch(url, options);\n }\n\n return fetched;\n }\n\n function getUserMedia(constraints) {\n\n if (!constraints) {\n\n constraints = {\n video: {\n deviceId: undefined\n },\n audio: {\n deviceId: undefined\n }\n };\n }\n\n console.info(logHeader, 'Request Stream To Input Devices With Constraints', constraints);\n\n return navigator.mediaDevices.getUserMedia(constraints)\n .then(function (stream) {\n\n console.info(logHeader, 'Received Media Stream From Input Device', stream);\n\n stream.getVideoTracks().forEach(function (track) {\n console.info(logHeader, 'Video Track Settings From Input Stream', track.getSettings());\n });\n\n instance.inputStream = stream;\n\n let elem = instance.videoElement;\n\n // Attach stream to video element when video element is provided.\n if (elem) {\n\n elem.srcObject = stream;\n\n elem.onloadedmetadata = function (e) {\n\n elem.play();\n };\n }\n\n return new Promise(function (resolve) {\n\n resolve(stream);\n });\n })\n .catch(function (error) {\n\n console.error(logHeader, 'Can\\'t Get Media Stream From Input Device', error);\n errorHandler(error);\n\n return new Promise(function (resolve, reject) {\n reject(error);\n });\n });\n }\n\n function getDisplayMedia(constraints) {\n\n if (!constraints) {\n constraints = {};\n }\n\n console.info(logHeader, 'Request Stream To Display With Constraints', constraints);\n\n return navigator.mediaDevices.getDisplayMedia(constraints)\n .then(function (stream) {\n\n console.info(logHeader, 'Received Media Stream From Display', stream);\n\n instance.inputStream = stream;\n\n let elem = instance.videoElement;\n\n // Attach stream to video element when video element is provided.\n if (elem) {\n\n elem.srcObject = stream;\n\n elem.onloadedmetadata = function (e) {\n\n elem.play();\n };\n }\n\n return new Promise(function (resolve) {\n\n resolve(stream);\n });\n })\n .catch(function (error) {\n\n console.error(logHeader, 'Can\\'t Get Media Stream From Display', error);\n errorHandler(error);\n\n return new Promise(function (resolve, reject) {\n reject(error);\n });\n });\n }\n\n function setMediaStream(stream) {\n // Check if a valid stream is provided\n if (!stream || !(stream instanceof MediaStream)) {\n\n const error = new Error(\"Invalid MediaStream provided\");\n console.error(logHeader, 'Invalid MediaStream', error);\n errorHandler(error);\n\n return new Promise(function (resolve, reject) {\n reject(error);\n });\n }\n\n console.info(logHeader, 'Received Media Stream', stream);\n\n instance.inputStream = stream;\n\n let elem = instance.videoElement;\n\n // Attach stream to video element when video element is provided.\n if (elem) {\n elem.srcObject = stream;\n\n elem.onloadedmetadata = function (e) {\n elem.play();\n };\n }\n\n return new Promise(function (resolve) {\n resolve(stream);\n });\n }\n\n // From https://webrtchacks.com/limit-webrtc-bandwidth-sdp/\n function setBitrateLimit(sdp, media, bitrate) {\n\n let lines = sdp.split('\\r\\n');\n let line = -1;\n\n for (let i = 0; i < lines.length; i++) {\n if (lines[i].indexOf('m=' + media) === 0) {\n line = i;\n break;\n }\n }\n if (line === -1) {\n // Could not find the m line for media\n return sdp;\n }\n\n // Pass the m line\n line++;\n\n // Skip i and c lines\n while (lines[line].indexOf('i=') === 0 || lines[line].indexOf('c=') === 0) {\n\n line++;\n }\n\n // If we're on a b line, replace it\n if (lines[line].indexOf('b') === 0) {\n\n lines[line] = 'b=AS:' + bitrate;\n\n return lines.join('\\r\\n');\n }\n\n // Add a new b line\n let newLines = lines.slice(0, line)\n\n newLines.push('b=AS:' + bitrate)\n newLines = newLines.concat(lines.slice(line, lines.length))\n\n return newLines.join('\\r\\n')\n }\n\n function normalizeFrameRate(frameRate) {\n if (Number.isInteger(frameRate)) {\n return frameRate;\n }\n return parseFloat(frameRate).toFixed(2);\n }\n\n function initWebSocket(endpointUrl) {\n\n if (!endpointUrl) {\n errorHandler('endpointUrl is required');\n return;\n }\n\n let webSocket = null;\n\n try {\n\n webSocket = new WebSocket(endpointUrl);\n } catch (error) {\n\n errorHandler(error);\n }\n\n\n instance.webSocket = webSocket;\n\n webSocket.onopen = function () {\n\n // Request offer at the first time.\n sendMessage(webSocket, {\n command: 'request_offer'\n });\n };\n\n webSocket.onmessage = function (e) {\n\n let message = JSON.parse(e.data);\n\n if (message.error) {\n console.error('webSocket.onmessage', message.error);\n errorHandler(message.error);\n }\n\n if (message.command === 'offer') {\n\n // OME returns offer. Start create peer connection.\n createPeerConnection(\n message.id,\n message.peer_id,\n message.sdp,\n message.candidates,\n message.ice_servers\n );\n }\n };\n\n webSocket.onerror = function (error) {\n\n console.error('webSocket.onerror', error);\n errorHandler(error);\n };\n\n webSocket.onclose = function (e) {\n\n if (!instance.webSocketClosedByUser) {\n\n if (instance.callbacks.connectionClosed) {\n instance.callbacks.connectionClosed('websocket', e);\n }\n }\n };\n\n }\n\n async function startWhip(endpointUrl) {\n\n if (instance.peerConnection) {\n console.error('Connection already established');\n errorHandler('Connection already established');\n return;\n }\n\n const peerConnectionConfig = {\n bundlePolicy: \"max-bundle\"\n };\n\n if (instance.connectionConfig.iceServers) {\n\n // first priority using ice servers from local config.\n peerConnectionConfig.iceServers = instance.connectionConfig.iceServers;\n\n if (instance.connectionConfig.iceTransportPolicy) {\n\n peerConnectionConfig.iceTransportPolicy = instance.connectionConfig.iceTransportPolicy;\n }\n } else {\n // last priority using default ice servers.\n\n if (instance.connectionConfig.iceTransportPolicy) {\n\n peerConnectionConfig.iceTransportPolicy = instance.connectionConfig.iceTransportPolicy;\n }\n }\n\n console.info(logHeader, 'Create Peer Connection With Config', peerConnectionConfig);\n\n const peerConnection = new RTCPeerConnection(peerConnectionConfig);\n\n instance.peerConnection = peerConnection;\n\n if (!instance.inputStream) {\n console.error('No input stream in OvenLiveKit');\n errorHandler('No input stream in OvenLiveKit');\n return;\n }\n\n for (const track of instance.inputStream.getTracks()) {\n console.log(logHeader, 'Adding track: ', track);\n\n const transceiverConfig = {\n direction: 'sendonly'\n };\n\n // Add simulcast layers if configured\n const simulcastConfig = instance.connectionConfig.simulcast;\n\n if (track.kind === 'video' && simulcastConfig && simulcastConfig.length > 0) {\n\n if (!transceiverConfig.sendEncodings) {\n transceiverConfig.sendEncodings = [];\n }\n\n for (let i = 0; i < simulcastConfig.length; i++) {\n\n const layer = {\n rid: i,\n active: true,\n // VP9/AV1 simulcast in Chrome requires an explicit scalabilityMode together with\n // scaleResolutionDownBy; without it, multiple encodings fall back to legacy SVC and\n // extra rids are dropped. Default to L1T1 (overridable via simulcastConfig[i]).\n // Multiple active encodings only allow L1T1/L1T2/L1T3 and all layers must share the same value.\n scalabilityMode: 'L1T1',\n ...simulcastConfig[i]\n };\n\n console.log(logHeader, `Adding simulcast layer to: ${track.kind}`, layer);\n\n transceiverConfig.sendEncodings.push(layer);\n }\n }\n\n if (track.kind === 'video' && instance.connectionConfig.maxVideoBitrate) {\n\n if (!transceiverConfig.sendEncodings) {\n transceiverConfig.sendEncodings = [];\n }\n\n transceiverConfig.sendEncodings.push({\n maxBitrate: instance.connectionConfig.maxVideoBitrate * 1000,\n });\n\n console.log(logHeader, `Setting max bitrate to: ${instance.connectionConfig.maxVideoBitrate} kbps`);\n }\n\n peerConnection.addTransceiver(track, transceiverConfig);\n }\n\n peerConnection.oniceconnectionstatechange = function (e) {\n\n let state = peerConnection.iceConnectionState;\n\n if (instance.callbacks.iceStateChange) {\n\n console.info(logHeader, 'ICE State', '[' + state + ']');\n instance.callbacks.iceStateChange(state);\n }\n\n if (state === 'connected') {\n\n if (instance.callbacks.connected) {\n instance.callbacks.connected(e);\n }\n }\n\n if (state === 'failed') {\n\n if (instance.callbacks.connectionClosed) {\n console.error(logHeader, 'Ice connection failed', e);\n instance.callbacks.errorHandler(e);\n }\n }\n\n if (state === 'disconnected' || state === 'closed') {\n\n if (instance.callbacks.connectionClosed) {\n console.error(logHeader, 'Ice connection disconnected or closed', e);\n instance.callbacks.connectionClosed('ice', e);\n }\n }\n }\n\n const offer = await peerConnection.createOffer();\n console.log(logHeader, 'Offer SDP from browser: ', offer.sdp);\n\n if (instance.connectionConfig.sdp && instance.connectionConfig.sdp.appendFmtp) {\n offer.sdp = appendFmtp(offer.sdp);\n }\n\n if (instance.connectionConfig.preferredVideoFormat) {\n offer.sdp = setPreferredVideoFormat(offer.sdp, instance.connectionConfig.preferredVideoFormat);\n } else {\n // default to H264\n offer.sdp = setPreferredVideoFormat(offer.sdp, 'H264');\n }\n\n const headers = {\n \"Content-Type\": \"application/sdp\"\n };\n\n // Set Oven-Capabilities header based on video track settings and simulcast layers.\n const videoTracks = instance.inputStream.getVideoTracks();\n\n if (videoTracks && videoTracks.length === 1) {\n\n const track = videoTracks[0];\n const settings = track.getSettings();\n\n console.log(logHeader, 'Video track settings for Oven-Capabilities:', settings);\n\n const width = settings.width;\n const height = settings.height;\n const frameRate = settings.frameRate;\n\n const simulcastConfig = instance.connectionConfig.simulcast;\n const layerCount = simulcastConfig ? simulcastConfig.length : 0;\n\n if (layerCount >= 2) {\n\n const parts = [];\n\n for (let i = 0; i < layerCount; i++) {\n\n const layer = simulcastConfig[i] || {};\n const rid = layer.rid !== undefined ? layer.rid : i;\n\n const scale = parseFloat(layer.scaleResolutionDownBy);\n\n if (!isNaN(scale) && scale > 0 && typeof width === 'number' && typeof height === 'number') {\n const scaledWidth = Math.floor(Math.floor(width / scale) / 2) * 2;\n const scaledHeight = Math.floor(Math.floor(height / scale) / 2) * 2;\n\n parts.push(`rid:${rid}:max_width=${scaledWidth}`);\n parts.push(`rid:${rid}:max_height=${scaledHeight}`);\n }\n\n if (frameRate) {\n parts.push(`rid:${rid}:max_fps=${normalizeFrameRate(frameRate)}`);\n }\n }\n\n if (parts.length > 0) {\n console.log(logHeader, `Setting Oven-Capabilities header: ${parts.join(', ')}`);\n headers['Oven-Capabilities'] = parts.join(', ');\n }\n } else {\n\n if (typeof width === 'number' && typeof height === 'number') {\n\n const parts = [`max_width=${width}`, `max_height=${height}`];\n\n if (frameRate) {\n parts.push(`max_fps=${normalizeFrameRate(frameRate)}`);\n }\n\n console.log(logHeader, `Setting Oven-Capabilities header: ${parts.join(', ')}`);\n headers['Oven-Capabilities'] = parts.join(', ');\n }\n }\n } else {\n\n if (!videoTracks || videoTracks.length === 0) {\n console.log(logHeader, 'No video tracks found, skipping Oven-Capabilities header.');\n }\n\n if (videoTracks && videoTracks.length > 1) {\n console.log(logHeader, `Multiple (${videoTracks.length}) video tracks found, skipping Oven-Capabilities header.`);\n }\n }\n\n if (instance.connectionConfig.httpHeaders) {\n Object.assign(headers, instance.connectionConfig.httpHeaders);\n }\n\n console.log(logHeader, 'Offer SDP modified by OvenLiveKit: ', offer.sdp);\n\n const fetched = await fetchWithRedirect(endpointUrl, {\n method: \"POST\",\n body: offer.sdp,\n headers\n });\n\n if (!fetched.ok) {\n console.error('Failed to fetch', fetched.status);\n errorHandler(`Failed to fetch ${fetched.status}`);\n closePeerConnection();\n return;\n }\n\n if (!fetched.headers.get(\"location\")) {\n console.error('No location header on answer response');\n errorHandler('No location header on answer response');\n return;\n }\n\n // update endpointUrl\n instance.endpointUrl = fetched.url;\n console.log(logHeader, 'Endpoint Url: ', instance.endpointUrl);\n\n const locationHeaderValue = fetched.headers.get(\"location\");\n console.log(logHeader, 'Location header ', locationHeaderValue);\n\n instance.resourceUrl = new URL(locationHeaderValue, fetched.url).toString();\n console.log(logHeader, 'Resource URL: ', instance.resourceUrl);\n\n\n if (fetched.headers.get(\"Link\")) {\n console.log(logHeader, 'Link Header Found. Update ');\n\n const links = fetched.headers.get(\"Link\").split(',').map(link => link.trim());\n\n // OvenMediaEngine returns turn servers in Link header with rel=\"ice-server\".\n // For example: <turn:119.196.229.2:3478?transport=tcp>; rel=\"ice-server\"; username=\"user\"; credential=\"cred\"; credential-type=\"password\"\n const turnIceServers = [];\n\n links.forEach(link => {\n\n console.log(logHeader, 'Link: ', link);\n const urlsMatch = link.match(/<([^>]+)>/);\n const relMatch = link.match(/rel=\"([^\"]+)\"/);\n const usernameMatch = link.match(/username=\"([^\"]+)\"/);\n const credentialMatch = link.match(/credential=\"([^\"]+)\"/);\n const credentialTypeMatch = link.match(/credential-type=\"([^\"]+)\"/);\n\n if (urlsMatch && relMatch && relMatch[1] === 'ice-server') {\n\n const iceServer = {\n urls: urlsMatch[1]\n };\n\n if (usernameMatch) {\n iceServer.username = usernameMatch[1];\n };\n\n if (credentialMatch) {\n iceServer.credential = credentialMatch[1];\n };\n\n if (credentialTypeMatch) {\n iceServer.credentialType = credentialTypeMatch[1];\n }\n\n turnIceServers.push(iceServer);\n }\n });\n\n console.log(logHeader, 'Parsed ICE Servers from Link header: ', turnIceServers);\n\n // update peer connection with new turn servers from OME.\n if (turnIceServers.length > 0) {\n const pcConfig = peerConnection.getConfiguration();\n\n // only update ice servers when local config does not have ice servers.\n if (!instance.connectionConfig.iceServers) {\n pcConfig.iceServers = turnIceServers;\n console.log(logHeader, 'Updated Peer Connection ICE Servers from Link header');\n }\n\n // set ice transport policy to relay if not set in local config.\n if (!instance.connectionConfig.iceTransportPolicy) {\n\n if (getTransportQuery(instance.endpointUrl) == 'all') {\n pcConfig.iceTransportPolicy = 'all';\n console.log(logHeader, 'Set Peer Connection iceTransportPolicy to all');\n } else {\n pcConfig.iceTransportPolicy = 'relay';\n console.log(logHeader, 'Set Peer Connection iceTransportPolicy to relay');\n }\n }\n\n peerConnection.setConfiguration(pcConfig);\n }\n }\n\n const answer = await fetched.text();\n console.log(logHeader, 'Response Answer SDP: ', answer);\n\n try {\n await peerConnection.setLocalDescription(offer);\n } catch (error) {\n console.error('peerConnection.setLocalDescription', error);\n errorHandler(error);\n }\n\n try {\n await peerConnection.setRemoteDescription({\n type: \"answer\",\n sdp: answer\n });\n } catch (error) {\n console.error('peerConnection.setRemoteDescription', error);\n errorHandler(error);\n }\n }\n\n async function stopWhip() {\n\n if (!instance.peerConnection) {\n console.error('No connection to close');\n errorHandler('No connection to close');\n return;\n }\n\n closePeerConnection();\n\n if (instance.resourceUrl) {\n\n const headers = {\n };\n\n if (instance.connectionConfig.httpHeaders) {\n Object.assign(headers, instance.connectionConfig.httpHeaders);\n }\n\n await fetchWithRedirect(instance.resourceUrl, {\n method: \"DELETE\",\n headers\n });\n }\n }\n\n function appendFmtp(sdp) {\n\n const fmtpStr = instance.connectionConfig.sdp.appendFmtp;\n\n const lines = sdp.split('\\r\\n');\n const payloads = [];\n\n for (let i = 0; i < lines.length; i++) {\n\n if (lines[i].indexOf('m=video') === 0) {\n\n let tokens = lines[i].split(' ')\n\n for (let j = 3; j < tokens.length; j++) {\n\n payloads.push(tokens[j]);\n }\n\n break;\n }\n }\n\n for (let i = 0; i < payloads.length; i++) {\n\n let fmtpLineFound = false;\n\n for (let j = 0; j < lines.length; j++) {\n\n if (lines[j].indexOf('a=fmtp:' + payloads[i]) === 0) {\n fmtpLineFound = true;\n lines[j] += ';' + fmtpStr;\n }\n }\n\n if (!fmtpLineFound) {\n\n for (let j = 0; j < lines.length; j++) {\n\n if (lines[j].indexOf('a=rtpmap:' + payloads[i]) === 0) {\n\n lines[j] += '\\r\\na=fmtp:' + payloads[i] + ' ' + fmtpStr;\n }\n }\n }\n }\n\n return lines.join('\\r\\n')\n }\n\n function appendOrientation(sdp) {\n\n const lines = sdp.split('\\r\\n');\n const payloads = [];\n\n for (let i = 0; i < lines.length; i++) {\n\n if (lines[i].indexOf('m=video') === 0) {\n\n let tokens = lines[i].split(' ')\n\n for (let j = 3; j < tokens.length; j++) {\n\n payloads.push(tokens[j]);\n }\n\n break;\n }\n }\n\n for (let i = 0; i < payloads.length; i++) {\n\n for (let j = 0; j < lines.length; j++) {\n\n if (lines[j].indexOf('a=rtpmap:' + payloads[i]) === 0) {\n\n lines[j] += '\\r\\na=extmap:' + payloads[i] + ' urn:3gpp:video-orientation';\n }\n }\n }\n\n return lines.join('\\r\\n')\n }\n\n function createPeerConnection(id, peerId, offer, candidates, iceServers) {\n\n let peerConnectionConfig = {};\n\n if (instance.connectionConfig.iceServers) {\n\n // first priority using ice servers from user config.\n peerConnectionConfig.iceServers = instance.connectionConfig.iceServers;\n\n if (instance.connectionConfig.iceTransportPolicy) {\n\n peerConnectionConfig.iceTransportPolicy = instance.connectionConfig.iceTransportPolicy;\n }\n } else if (iceServers) {\n\n // second priority using ice servers from ome and force using TCP\n peerConnectionConfig.iceServers = [];\n\n for (let i = 0; i < iceServers.length; i++) {\n\n let iceServer = iceServers[i];\n\n let regIceServer = {};\n\n regIceServer.urls = iceServer.urls;\n\n let hasWebSocketUrl = false;\n let webSocketUrl = generateDomainFromUrl(instance.endpointUrl);\n\n for (let j = 0; j < regIceServer.urls.length; j++) {\n\n let serverUrl = regIceServer.urls[j];\n\n if (serverUrl.indexOf(webSocketUrl) > -1) {\n hasWebSocketUrl = true;\n break;\n }\n }\n\n if (!hasWebSocketUrl) {\n\n if (regIceServer.urls.length > 0) {\n\n let cloneIceServer = regIceServer.urls[0];\n let ip = findIp(cloneIceServer);\n\n if (webSocketUrl && ip) {\n regIceServer.urls.push(cloneIceServer.replace(ip, webSocketUrl));\n }\n }\n }\n\n regIceServer.username = iceServer.user_name;\n regIceServer.credential = iceServer.credential;\n\n peerConnectionConfig.iceServers.push(regIceServer);\n }\n\n if (instance.connectionConfig.iceTransportPolicy) {\n\n peerConnectionConfig.iceTransportPolicy = instance.connectionConfig.iceTransportPolicy;\n } else {\n if (getTransportQuery(instance.endpointUrl) == 'all') {\n peerConnectionConfig.iceTransportPolicy = 'all';\n console.log(logHeader, 'Set Peer Connection iceTransportPolicy to all');\n } else {\n peerConnectionConfig.iceTransportPolicy = 'relay';\n console.log(logHeader, 'Set Peer Connection iceTransportPolicy to relay');\n }\n }\n }\n\n let advancedSetting = {\n optional: [\n {\n googHighStartBitrate: {\n exact: !0\n }\n },\n {\n googPayloadPadding: {\n exact: !0\n }\n },\n {\n googScreencastMinBitrate: {\n exact: 500\n }\n },\n {\n enableDscp: {\n exact: true\n }\n }\n ]\n };\n\n console.info(logHeader, 'Create Peer Connection With Config', peerConnectionConfig);\n\n let peerConnection = new RTCPeerConnection(peerConnectionConfig);\n\n instance.peerConnection = peerConnection;\n\n // set local stream\n instance.inputStream.getTracks().forEach(function (track) {\n\n console.info(logHeader, 'Add Track To Peer Connection', track);\n peerConnection.addTrack(track, instance.inputStream);\n });\n\n if (instance.connectionConfig.maxVideoBitrate) {\n\n // if bandwith limit is set. modify sdp from ome to limit acceptable bandwidth of ome\n offer.sdp = setBitrateLimit(offer.sdp, 'video', instance.connectionConfig.maxVideoBitrate);\n }\n\n if (instance.connectionConfig.sdp && instance.connectionConfig.sdp.appendFmtp) {\n\n offer.sdp = appendFmtp(offer.sdp);\n }\n\n if (instance.connectionConfig.preferredVideoFormat) {\n offer.sdp = setPreferredVideoFormat(offer.sdp, instance.connectionConfig.preferredVideoFormat)\n }\n\n\n // offer.sdp = appendOrientation(offer.sdp);\n console.info(logHeader, 'Modified offer sdp\\n\\n' + offer.sdp);\n\n peerConnection.setRemoteDescription(new RTCSessionDescription(offer))\n .then(function () {\n\n peerConnection.createAnswer()\n .then(function (answer) {\n\n if (instance.connectionConfig.sdp && instance.connectionConfig.sdp.appendFmtp) {\n\n answer.sdp = appendFmtp(answer.sdp);\n }\n\n if (instance.connectionConfig.preferredVideoFormat) {\n answer.sdp = setPreferredVideoFormat(answer.sdp, instance.connectionConfig.preferredVideoFormat)\n }\n\n console.info(logHeader, 'Modified answer sdp\\n\\n' + answer.sdp);\n\n peerConnection.setLocalDescription(answer)\n .then(function () {\n\n sendMessage(instance.webSocket, {\n id: id,\n peer_id: peerId,\n command: 'answer',\n sdp: answer\n });\n })\n .catch(function (error) {\n\n console.error('peerConnection.setLocalDescription', error);\n errorHandler(error);\n });\n })\n .catch(function (error) {\n\n console.error('peerConnection.createAnswer', error);\n errorHandler(error);\n });\n })\n .catch(function (error) {\n\n console.error('peerConnection.setRemoteDescription', error);\n errorHandler(error);\n });\n\n if (candidates) {\n\n addIceCandidate(peerConnection, candidates);\n }\n\n peerConnection.onicecandidate = function (e) {\n\n if (e.candidate && e.candidate.candidate) {\n\n sendMessage(instance.webSocket, {\n id: id,\n peer_id: peerId,\n command: 'candidate',\n candidates: [e.candidate]\n });\n }\n };\n\n peerConnection.oniceconnectionstatechange = function (e) {\n\n let state = peerConnection.iceConnectionState;\n\n if (instance.callbacks.iceStateChange) {\n\n console.info(logHeader, 'ICE State', '[' + state + ']');\n instance.callbacks.iceStateChange(state);\n }\n\n if (state === 'connected') {\n\n if (instance.callbacks.connected) {\n instance.callbacks.connected(e);\n }\n }\n\n if (state === 'failed' || state === 'disconnected' || state === 'closed') {\n\n if (instance.callbacks.connectionClosed) {\n console.error(logHeader, 'Ice connection closed', e);\n instance.callbacks.connectionClosed('ice', e);\n }\n }\n }\n }\n\n function addIceCandidate(peerConnection, candidates) {\n\n for (let i = 0; i < candidates.length; i++) {\n\n if (candidates[i] && candidates[i].candidate) {\n\n let basicCandidate = candidates[i];\n\n peerConnection.addIceCandidate(new RTCIceCandidate(basicCandidate))\n .then(function () {\n\n })\n .catch(function (error) {\n\n console.error('peerConnection.addIceCandidate', error);\n errorHandler(error);\n });\n }\n }\n }\n\n function closePeerConnection() {\n if (instance.peerConnection) {\n\n // remove tracks from peer connection\n instance.peerConnection.getSenders().forEach(function (sender) {\n instance.peerConnection.removeTrack(sender);\n });\n\n instance.peerConnection.close();\n instance.peerConnection = null;\n delete instance.peerConnection;\n }\n }\n\n function closeWebSocket() {\n\n if (instance.webSocket) {\n\n instance.webSocket.close();\n instance.webSocket = null;\n delete instance.webSocket;\n }\n }\n\n function closeInputStream() {\n // release video, audio stream\n if (instance.inputStream) {\n\n instance.inputStream.getTracks().forEach(track => {\n\n track.stop();\n instance.inputStream.removeTrack(track);\n });\n\n if (instance.videoElement) {\n instance.videoElement.srcObject = null;\n }\n\n instance.inputStream = null;\n delete instance.inputStream;\n }\n }\n\n // instance methods\n instance.attachMedia = function (videoElement) {\n\n instance.videoElement = videoElement;\n };\n\n instance.getUserMedia = function (constraints) {\n\n return getUserMedia(constraints);\n };\n\n instance.getDisplayMedia = function (constraints) {\n\n return getDisplayMedia(constraints);\n };\n\n instance.setMediaStream = function (stream) {\n\n return setMediaStream(stream);\n };\n\n instance.startStreaming = function (endpointUrl, connectionConfig) {\n\n console.info(logEventHeader, `Start Streaming to ${endpointUrl} with connectionConfig`, connectionConfig);\n\n if (!endpointUrl) {\n console.error('endpointUrl is required');\n errorHandler('endpointUrl is required');\n return;\n }\n\n instance.endpointUrl = endpointUrl;\n\n if (connectionConfig) {\n instance.connectionConfig = connectionConfig;\n }\n\n try {\n\n const protocol = new URL(endpointUrl).protocol;\n\n if (protocol === 'wss:' || protocol === 'ws:') {\n\n instance.streamingMode = 'webrtc';\n initWebSocket(endpointUrl);\n } else if (protocol === 'https:' || protocol === 'http:') {\n\n instance.streamingMode = 'whip';\n startWhip(endpointUrl);\n } else {\n console.error('Invalid protocol', error);\n errorHandler(error);\n }\n\n } catch (error) {\n console.error('Cannot parse connection URL', error);\n errorHandler(error);\n }\n };\n\n instance.stopStreaming = async function () {\n\n if (instance.streamingMode === 'webrtc') {\n\n instance.webSocketClosedByUser = true;\n\n closeWebSocket();\n closePeerConnection();\n } else if (instance.streamingMode === 'whip') {\n\n await stopWhip();\n }\n\n if (instance.callbacks.connectionClosed) {\n console.log(logHeader, 'Connection closed by user');\n instance.callbacks.connectionClosed('user', 'Connection closed by user');\n }\n };\n\n instance.remove = function () {\n\n if (instance.streamingMode === 'webrtc') {\n\n instance.webSocketClosedByUser = true;\n\n closeWebSocket();\n closePeerConnection();\n } else if (instance.streamingMode === 'whip') {\n stopWhip();\n }\n\n closeInputStream();\n\n console.info(logEventHeader, 'Removed');\n\n };\n}\n\nOvenLiveKit.getVersion = function () {\n return version;\n}\n\n// static methods\nOvenLiveKit.create = function (options) {\n\n console.info(logEventHeader, 'Create WebRTC Input ' + version);\n\n let instance = {};\n\n instance.webSocketClosedByUser = false;\n\n initConfig(instance, options);\n addMethod(instance);\n\n return instance;\n};\n\nOvenLiveKit.getDevices = async function (type = 'both') {\n\n try {\n // First check both audio and video sources are available.\n await getStreamForDeviceCheck(type);\n } catch (e) {\n\n console.warn(logHeader, 'Can not find Video and Audio devices.', e);\n\n let videoFound = null;\n let audioFound = null;\n\n try {\n videoFound = await getStreamForDeviceCheck('video');\n } catch (e) {\n console.warn(logHeader, 'Can not find Video devices.', e);\n }\n\n try {\n audioFound = await getStreamForDeviceCheck('audio');\n } catch (e) {\n console.warn(logHeader, 'Can not find Audio devices.', e);\n }\n\n if (!videoFound && !audioFound) {\n throw new Error('No input devices were found.');\n }\n }\n\n const deviceInfos = await getDevices();\n return gotDevices(deviceInfos)\n};\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (OvenLiveKit);\n\n\n//# sourceURL=webpack://OvenLiveKit/./src/OvenLiveKit.js?"); /***/ }) /******/ }); /************************************************************************/ /******/ // The require scope /******/ var __webpack_require__ = {}; /******/ /************************************************************************/ /******/ /* webpack/runtime/define property getters */ /******/ (() => { /******/ // define getter functions for harmony exports /******/ __webpack_require__.d = (exports, definition) => { /******/ for(var key in definition) { /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); /******/ } /******/ } /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ (() => { /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) /******/ })(); /******/ /******/ /* webpack/runtime/make namespace object */ /******/ (() => { /******/ // define __esModule on exports /******/ __webpack_require__.r = (exports) => { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ })(); /******/ /************************************************************************/ /******/ /******/ // startup /******/ // Load entry module and return exports /******/ // This entry module can't be inlined because the eval devtool is used. /******/ var __webpack_exports__ = {}; /******/ __webpack_modules__["./src/OvenLiveKit.js"](0, __webpack_exports__, __webpack_require__); /******/ __webpack_exports__ = __webpack_exports__["default"]; /******/ /******/ return __webpack_exports__; /******/ })() ; });