diff --git a/src/page/sample-mixer-pages/singular-live/.gitignore b/src/page/sample-mixer-pages/singular-live/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/src/page/sample-mixer-pages/singular-live/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/src/page/sample-mixer-pages/singular-live/README.md b/src/page/sample-mixer-pages/singular-live/README.md new file mode 100644 index 00000000..d2a42749 --- /dev/null +++ b/src/page/sample-mixer-pages/singular-live/README.md @@ -0,0 +1,62 @@ +# Red5 CEF Mixer Page with Singular.live Integration + +This is an example page which can be loaded by a Red5 CEF Mixer to integrate with `Singular.live` overlays in order to broadcast out a single mixed stream. + +# Requirements + +Before using this example, you will first need to have a [Singular.live account](https://www.singular.live/) and at least one `App` in your account dashboard that can be used. + +## App Token + +From your `Singular.live` account dashboard, you will see the available `App`s listed. Each `App` has an associated **Public App Token** associated and accessed by first selecting the target `App` from the dashboard, then selecting the `i` icon. + +This will show you the associated URLs and Token(s) of the target `App`. + +**You will need to provide this token to the CEF Mixer page in order to integrate with your Singular.live App.** + +> More information about `Singular.live` and Apps can be found at: [https://support.singular.live/hc/en-us/articles/360034417991-Beginner-s-Guide-to-Singular](https://support.singular.live/hc/en-us/articles/360034417991-Beginner-s-Guide-to-Singular). + +# Usage + +The Red5 CEF Mixer is instructed to load a specified page at a URL and broadcast a live stream to a target Red5 Server. This is quickly and easily done using the `controller` html provided from the `mixer` webapp. + +In order to have a CEF Mixer load this example page to integrate with `Singular.live`, you will first need to deploy the files contained in this repository to a remote location which can be loaded over SSL. + +> For the purposes of this document, we will say that the files contained in this repo and the `index.html` page is accessible at: `https://red5testing.com`. + +When instructing the CEF Mixer to load this example from `https://red5testing.com` it is important to provided some query parameters in order to integrate with `Singular.live`. + +## Query Parameters + +The following query parameters are recognized by the page: + +### Required + +- `sl_token` - The App Token from `Singular.live`. Please read the [Requirements](#requirements) section for accessing the token. +- `host` - The Red5 Server endpoint that has the live stream you wish to consume and overlay the content from `Singular.live`. +- `name` - The name of the stream on the `host` Red5 Server to consume. + +### Optional + +- `app` - The target webapp context that the stream resides in on the `host`. _Default is `live`._ +- `fit` - The `object-fit` style to apply to the playback stream. _Default is `cover`._ +- `vod` - A Flag to denote whether the stream to playback is VOD or not. _Default is `false`._ +- `vodURL` - The full URL to a VOD file to playback. _Default is `undefined`._ + +When specifying `vod=true`, the page will utilize [HLS.js](https://github.com/video-dev/hls.js/) to playback the HLS file in browsers that do not support native HLS. + +> If you provde the full URL for a VOD file using the `vodURL` query parameter, you do not need to define `vod=true` as well. Additionally, any `vodURL` provided is recommended to be url-encoded so as to avoid breaking the parsing of the query parameters. + +### Example URL + +The following example URL is one to provide to the CEF Mixer with a live playback of a stream named `stream1` accessible from the `live` webapp context on `https://myred5.com` with an overlay App token of `abcd123` from a Singular.live account: + +```sh +https://red5testing.com?sl_token=abcd1234&host=myred5.com&app=live&name=stream1 +``` + +The following example URL is one to provide to the CEF Mixer if the above stream is a VOD file residing on the same server: + +```sh +https://red5testing.com?sl_token=abcd1234&host=myred5.com&app=live&name=stream1&vod=true +``` diff --git a/src/page/sample-mixer-pages/singular-live/index.html b/src/page/sample-mixer-pages/singular-live/index.html new file mode 100644 index 00000000..8138fada --- /dev/null +++ b/src/page/sample-mixer-pages/singular-live/index.html @@ -0,0 +1,31 @@ + + + + + + Red5 & Singular.live + + + + + + +
+
+ +
+ + + +
+ + + + + diff --git a/src/page/sample-mixer-pages/singular-live/script/main.js b/src/page/sample-mixer-pages/singular-live/script/main.js new file mode 100644 index 00000000..d5f7af39 --- /dev/null +++ b/src/page/sample-mixer-pages/singular-live/script/main.js @@ -0,0 +1,180 @@ +/* +Copyright © 2015 Infrared5, Inc. All rights reserved. + +The accompanying code comprising examples for use solely in conjunction with Red5 Pro (the "Example Code") +is licensed to you by Infrared5 Inc. in consideration of your agreement to the following +license terms and conditions. Access, use, modification, or redistribution of the accompanying +code constitutes your acceptance of the following license terms and conditions. + +Permission is hereby granted, free of charge, to you to use the Example Code and associated documentation +files (collectively, the "Software") without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The Software shall be used solely in conjunction with Red5 Pro. Red5 Pro is licensed under a separate end +user license agreement (the "EULA"), which must be executed with Infrared5, Inc. +An example of the EULA can be found on our website at: https://account.red5pro.com/assets/LICENSE.txt. + +The above copyright notice and this license shall be included in all copies or portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL INFRARED5, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +import { query, isHostAnIPAddress } from './url-util.js' + +const { host, app, name, fit, vod, overlayToken, vodURL, pageURL } = query() +const container = document.querySelector('#app') + +const red5pro = window.red5prosdk +const singular = window.SingularGraphics +red5pro.setLogLevel('debug') + +let retryTimeout = 0 +const RETRY_DELAY = 1000 + +const options = { + class: 'overlay', + endpoint: 'http://app.singular.live', + interactive: false, + syncGraphics: true, + showPreloader: true, + aspect: '', +} + +const overlay = singular('SingularOverlay', options) +overlay.addListener('message', (type, params) => { + console.log('message', type, params) +}) +overlay.addListener('error', (type, params) => { + console.error('error', type, params) + showError( + `${type}: ${ + params + ? typeof params !== 'string' + ? JSON.stringify(params) + : params + : '' + }` + ) +}) +overlay.renderAppOutput(overlayToken, null, (success) => { + if (success) { + if (vod || vodURL) { + loadVOD() + } else if (pageURL) { + loadPage(pageURL) + } else { + loadLive() + } + } else { + showError("Couldn't load composition") + } +}) + +const loadLive = async () => { + clearTimeout(retryTimeout) + try { + document.querySelector('#red5pro-subscriber').style['object-fit'] = fit + const isSecure = !isHostAnIPAddress(host) + // CEF Mixer does not support WHIP/WHEP at the moment... + // const subscriber = new red5pro.WHEPClient() + const subscriber = new red5pro.RTCSubscriber() + subscriber.on('*', (event) => { + const { type } = event + if (type !== 'Subscribe.Time.Update') { + console.log(type) + if ( + type === 'Subscribe.Connection.Closed' || + type === 'Subscribe.Play.Unpublish' + ) { + subscriber.unsubscribe() + retryTimeout = setTimeout(loadLive, RETRY_DELAY) + } + } + }) + await subscriber.init({ + host, + protocol: isSecure ? 'wss' : 'ws', + port: isSecure ? 443 : 5080, + app: app || 'live', + streamName: name, + }) + await subscriber.subscribe() + } catch (e) { + console.error(e) + if (!vod) { + retryTimeout = setTimeout(loadLive, RETRY_DELAY) + } + } +} + +const loadVOD = async () => { + const element = document.getElementById('red5pro-subscriber') + const isSecure = !isHostAnIPAddress(host) + const protocol = isSecure ? 'https' : 'http' + const port = isSecure ? 443 : 5080 + const url = + vodURL || `${protocol}://${host}:${port}/${app || 'live'}/${name}.m3u8` + if (Hls.isSupported()) { + const hls = new Hls() + hls.attachMedia(element) + hls.on(Hls.Events.MEDIA_ATTACHED, () => { + hls.loadSource(url) + }) + hls.on(Hls.Events.MANIFEST_PARSED, () => { + try { + element.play() + } catch (e) { + console.error(e) + } + }) + hls.on(Hls.Events.ERROR, function (err, info) { + showError(`[Error]:: ${err}, ${JSON.stringify(info)}`) + }) + // Hook in and call overlay SDK methods here: + // videoPlaying + // videoBuffering + // videoFinished + // videoTime + element.onplay = () => overlay.videoPlaying() + element.onwaiting = () => overlay.videoBuffering() + element.onended = () => overlay.videoFinished() + element.ontimeupdate = () => overlay.videoTime(element.currentTime) + } else if (element.canPlayType('application/vnd.apple.mpegurl')) { + element.src = url + element.addEventListener('loadedmetadata', function () { + try { + element.play() + } catch (e) { + console.error(e) + } + }) + } else { + showError('Your browser does not support HLS video playback.') + } +} + +const loadPage = (url) => { + const videoContainer = document.querySelector('#video-container') + const iframe = document.createElement('iframe') + iframe.classList.add('overlay') + iframe.style['z-index'] = 0 + iframe.setAttribute('src', url) + iframe.setAttribute('width', '100%') + iframe.setAttribute('height', '100%') + iframe.setAttribute('frameborder', '0') + iframe.setAttribute('scrolling', 'no') + iframe.setAttribute('allowfullscreen', 'true') + container.insertBefore(iframe, videoContainer) + videoContainer.parentNode.removeChild(videoContainer) +} + +const showError = (message) => { + const container = document.querySelector('#error-container') + const error = document.querySelector('#error-message') + container.classList.remove('hidden') + error.textContent = message +} diff --git a/src/page/sample-mixer-pages/singular-live/script/simple.js b/src/page/sample-mixer-pages/singular-live/script/simple.js new file mode 100644 index 00000000..70c2c299 --- /dev/null +++ b/src/page/sample-mixer-pages/singular-live/script/simple.js @@ -0,0 +1,163 @@ +/* +Copyright © 2015 Infrared5, Inc. All rights reserved. + +The accompanying code comprising examples for use solely in conjunction with Red5 Pro (the "Example Code") +is licensed to you by Infrared5 Inc. in consideration of your agreement to the following +license terms and conditions. Access, use, modification, or redistribution of the accompanying +code constitutes your acceptance of the following license terms and conditions. + +Permission is hereby granted, free of charge, to you to use the Example Code and associated documentation +files (collectively, the "Software") without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The Software shall be used solely in conjunction with Red5 Pro. Red5 Pro is licensed under a separate end +user license agreement (the "EULA"), which must be executed with Infrared5, Inc. +An example of the EULA can be found on our website at: https://account.red5pro.com/assets/LICENSE.txt. + +The above copyright notice and this license shall be included in all copies or portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL INFRARED5, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +/* global Hls, SingularGraphics, red5prosdk */ +import { query, isHostAnIPAddress } from './url-util.js' + +const { host, app, name, fit, vod, overlayToken, vodURL, pageURL } = query() +const container = document.querySelector('#app') + +const red5pro = window.red5prosdk +const singular = window.SingularGraphics +red5pro.setLogLevel('debug') + +let retryTimeout = 0 +const RETRY_DELAY = 1000 + +const options = { + class: 'overlay', + endpoint: 'http://app.singular.live', + interactive: false, + syncGraphics: true, + showPreloader: true, + aspect: '', +} + +// > Quick and Simple +const loadLive = async () => { + clearTimeout(retryTimeout) + try { + document.querySelector('#red5pro-subscriber').style['object-fit'] = fit + const isSecure = !isHostAnIPAddress(host) + // CEF Mixer does not support WHIP/WHEP at the moment... + // const subscriber = new red5pro.WHEPClient() + const subscriber = new red5pro.RTCSubscriber() + subscriber.on('*', (event) => { + const { type } = event + if (type !== 'Subscribe.Time.Update') { + console.log(type) + if ( + type === 'Subscribe.Connection.Closed' || + type === 'Subscribe.Play.Unpublish' + ) { + subscriber.unsubscribe() + retryTimeout = setTimeout(loadLive, RETRY_DELAY) + } + } + }) + await subscriber.init({ + host, + protocol: isSecure ? 'wss' : 'ws', + port: isSecure ? 443 : 5080, + app: app || 'live', + streamName: name, + }) + await subscriber.subscribe() + } catch (e) { + console.error(e) + if (!vod) { + retryTimeout = setTimeout(loadLive, RETRY_DELAY) + } + } +} + +const loadVOD = async () => { + const element = document.getElementById('red5pro-subscriber') + const isSecure = !isHostAnIPAddress(host) + const protocol = isSecure ? 'https' : 'http' + const port = isSecure ? 443 : 5080 + const url = + vodURL || `${protocol}://${host}:${port}/${app || 'live'}/${name}.m3u8` + if (Hls.isSupported()) { + const hls = new Hls() + hls.attachMedia(element) + hls.on(Hls.Events.MEDIA_ATTACHED, () => { + hls.loadSource(url) + }) + hls.on(Hls.Events.MANIFEST_PARSED, () => { + try { + element.play() + } catch (e) { + console.error(e) + } + }) + hls.on(Hls.Events.ERROR, function (err, info) { + showError(`[Error]:: ${err}, ${JSON.stringify(info)}`) + }) + } else if (element.canPlayType('application/vnd.apple.mpegurl')) { + element.src = url + element.addEventListener('loadedmetadata', function () { + try { + element.play() + } catch (e) { + console.error(e) + } + }) + } else { + showError('Your browser does not support HLS video playback.') + } +} + +const loadPage = (url) => { + const videoContainer = document.querySelector('#video-container') + const iframe = document.createElement('iframe') + iframe.classList.add('overlay') + iframe.style['z-index'] = 0 + iframe.setAttribute('src', url) + iframe.setAttribute('width', '100%') + iframe.setAttribute('height', '100%') + iframe.setAttribute('frameborder', '0') + iframe.setAttribute('scrolling', 'no') + iframe.setAttribute('allowfullscreen', 'true') + container.insertBefore(iframe, videoContainer) + videoContainer.parentNode.removeChild(videoContainer) +} + +const showError = (message) => { + const container = document.querySelector('#error-container') + const error = document.querySelector('#error-message') + container.classList.remove('hidden') + error.textContent = message +} + +const load = async () => { + try { + const url = `https://app.singular.live/apiv2/controlapps/${overlayToken}` + const response = await fetch(url, { + method: 'GET', + }) + const json = await response.json() + const contentURL = json.outputUrl + const iframe = document.querySelector('#SingularOverlay') + iframe.onload = + vod || vodURL ? loadVOD : pageURL ? () => loadPage(pageURL) : loadLive + iframe.setAttribute('src', contentURL) + } catch (e) { + console.error(e) + showError(e.message) + } +} + +load() diff --git a/src/page/sample-mixer-pages/singular-live/script/url-util.js b/src/page/sample-mixer-pages/singular-live/script/url-util.js new file mode 100644 index 00000000..f0fc93f7 --- /dev/null +++ b/src/page/sample-mixer-pages/singular-live/script/url-util.js @@ -0,0 +1,81 @@ +/* +Copyright © 2015 Infrared5, Inc. All rights reserved. + +The accompanying code comprising examples for use solely in conjunction with Red5 Pro (the "Example Code") +is licensed to you by Infrared5 Inc. in consideration of your agreement to the following +license terms and conditions. Access, use, modification, or redistribution of the accompanying +code constitutes your acceptance of the following license terms and conditions. + +Permission is hereby granted, free of charge, to you to use the Example Code and associated documentation +files (collectively, the "Software") without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The Software shall be used solely in conjunction with Red5 Pro. Red5 Pro is licensed under a separate end +user license agreement (the "EULA"), which must be executed with Infrared5, Inc. +An example of the EULA can be found on our website at: https://account.red5pro.com/assets/LICENSE.txt. + +The above copyright notice and this license shall be included in all copies or portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL INFRARED5, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/** + * Utility for parsing query params. + * The following query parameters are supported: + * - host: The Red5 Pro Server hostname on which the live streams are located. + * - app: The app context on the Red5 Pro Server on which the live streams are located. + * - sm: A boolean value indicating whether the Red5 Pro Server is a Stream Manager. + * - overlay: The Singular.live overlay URL to load. + * + * @returns {object} Object containing the query params. + */ +export const query = () => { + const searchParams = new URLSearchParams(window.location.search) + const smOpt = searchParams.get('sm') + const vodOpt = searchParams.get('vod') + let host = searchParams.get('host') + ? decodeURIComponent(searchParams.get('host')) + : window.location.hostname + let app = searchParams.get('app') + ? decodeURIComponent(searchParams.get('app')) + : 'live' + let name = searchParams.get('name') + ? decodeURIComponent(searchParams.get('name')) + : 'stream1' + let fit = searchParams.get('fit') || 'cover' + let overlayToken = searchParams.get('sl_token') + ? decodeURIComponent(searchParams.get('sl_token')) + : undefined + let streamManager = smOpt ? smOpt.toLowerCase() === 'true' : false + let vod = vodOpt ? vodOpt.toLowerCase() === 'true' : false + let vodURL = searchParams.get('vod_url') + ? decodeURIComponent(searchParams.get('vod_url')) + : undefined + let pageURL = searchParams.get('page_url') + ? decodeURIComponent(searchParams.get('page_url')) + : undefined + return { + host, + app, + name, + fit, + vod, + overlayToken, + streamManager, + vodURL, + pageURL, + get: (key) => { + return searchParams.get(key) + }, + } +} + +const ipRegex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/ +export const isHostAnIPAddress = (host) => { + return ipRegex.test(host) +} diff --git a/src/page/sample-mixer-pages/singular-live/style/style.css b/src/page/sample-mixer-pages/singular-live/style/style.css new file mode 100644 index 00000000..6a695598 --- /dev/null +++ b/src/page/sample-mixer-pages/singular-live/style/style.css @@ -0,0 +1,104 @@ +/* +Copyright © 2015 Infrared5, Inc. All rights reserved. + +The accompanying code comprising examples for use solely in conjunction with Red5 Pro (the "Example Code") +is licensed to you by Infrared5 Inc. in consideration of your agreement to the following +license terms and conditions. Access, use, modification, or redistribution of the accompanying +code constitutes your acceptance of the following license terms and conditions. + +Permission is hereby granted, free of charge, to you to use the Example Code and associated documentation +files (collectively, the "Software") without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The Software shall be used solely in conjunction with Red5 Pro. Red5 Pro is licensed under a separate end +user license agreement (the "EULA"), which must be executed with Infrared5, Inc. +An example of the EULA can be found on our website at: https://account.red5pro.com/assets/LICENSE.txt. + +The above copyright notice and this license shall be included in all copies or portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL INFRARED5, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 100vw; + min-height: 100vh; + position: relative; + overflow: hidden; +} + +.hidden { + display: none!important; +} + +#video-container, .overlay { + width: 100vw; + height: 100vh; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + z-index: 0; +} + +#video-container { + background-color: black; +} + +.overlay { + z-index: 1; +} + +#red5pro-subscriber { + width: 100%; + height: 100%; + object-fit: cover; +} + +#error-container { + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.35); + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; +} + +#error-message { + background-color: #ddd; + width: 45%; + height: fit-content; + padding: 1rem; + font-size: 1.6rem; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } +} diff --git a/src/page/testbed-menu-mixer-sample-pages.html b/src/page/testbed-menu-mixer-sample-pages.html index f6bb688c..e606fa18 100644 --- a/src/page/testbed-menu-mixer-sample-pages.html +++ b/src/page/testbed-menu-mixer-sample-pages.html @@ -32,6 +32,7 @@

Red5 Pro HTML Sample Mixer Composition Pages

  • 7x7 Grid Layout
  • Dynamic NxN Grid Layout
  • Conference Layout
  • +
  • Singular.live Overlay