├── img └── tokens.png ├── packages ├── millicast-sdk │ ├── .npmignore │ ├── tests │ │ ├── unit │ │ │ ├── samples │ │ │ │ ├── pic-1.bin │ │ │ │ ├── pic-2.bin │ │ │ │ ├── pic-3.bin │ │ │ │ └── pic-4.bin │ │ │ ├── __mocks__ │ │ │ │ ├── MockBrowser.js │ │ │ │ ├── jwt-decode.js │ │ │ │ ├── Fetch.js │ │ │ │ ├── MockMediaStream.js │ │ │ │ └── MockRTCPeerConnectionNoConnectionState.js │ │ │ ├── BaseWebRTC.steps.js │ │ │ ├── SetRemoteDescription.steps.js │ │ │ ├── PeerStats.steps.js │ │ │ ├── GetPeerStatus.steps.js │ │ │ ├── ManagePeerConnection.steps.js │ │ │ ├── ChangeMediaTrack.steps.js │ │ │ ├── LoggerLevels.steps.js │ │ │ ├── LoggerHandlers.steps.js │ │ │ ├── SdpStereo.steps.js │ │ │ ├── SdpAbsCaptureTime.steps.js │ │ │ ├── ManageSignaling.steps.js │ │ │ └── UpdateBitrateWebRTC.steps.js │ │ ├── e2e │ │ │ ├── utils │ │ │ │ ├── test-environment-sample.js │ │ │ │ └── Media.js │ │ │ ├── View.html │ │ │ ├── Puppeteer.steps.js │ │ │ ├── PuppeteerJest.html │ │ │ ├── FunctionalPublish.steps.js │ │ │ ├── Publish.html │ │ │ └── ViewTest.js │ │ └── features │ │ │ ├── SdpStereo.feature │ │ │ ├── SdpAbsCaptureTime.feature │ │ │ ├── BaseWebRTC.feature │ │ │ ├── SdpRemoveLines.feature │ │ │ ├── SetRemoteDescription.feature │ │ │ ├── FunctionalPublish.feature │ │ │ ├── Puppeteer.feature │ │ │ ├── ChangeMediaTrack.feature │ │ │ ├── SdpBitrate.feature │ │ │ ├── ManagePeerConnection.feature │ │ │ ├── GetPeerStatus.feature │ │ │ ├── LoggerHandlers.feature │ │ │ ├── SdpSimulcast.feature │ │ │ ├── PeerStats.feature │ │ │ ├── LoggerLevels.feature │ │ │ ├── UpdateBitrateWebRTC.feature │ │ │ ├── PeerConnectionEvent.feature │ │ │ ├── ManageSignaling.feature │ │ │ ├── SdpMultiopus.feature │ │ │ ├── LoggerDiagnose.feature │ │ │ ├── LoggerHistory.feature │ │ │ ├── OfferSubscribingStream.feature │ │ │ ├── GetPublisherConnectionPath.feature │ │ │ ├── View.feature │ │ │ ├── GetSubscriberConnectionPath.feature │ │ │ ├── GetCapabilities.feature │ │ │ ├── ViewerReconnection.feature │ │ │ ├── PublisherReconnection.feature │ │ │ ├── Publish.feature │ │ │ ├── SetLocalDescription.feature │ │ │ └── OfferPublishingStream.feature │ ├── src │ │ ├── utils │ │ │ ├── FetchError.js │ │ │ ├── StringUtils.js │ │ │ ├── StreamTransform.js │ │ │ ├── ObjectUtils.js │ │ │ ├── BitStreamReader.js │ │ │ ├── UserAgent.js │ │ │ └── Diagnostics.js │ │ └── index.js │ ├── rollup.config.js │ ├── vite.config.debug.mjs │ ├── vite.config.mjs │ ├── jest_resolver.js │ ├── tsconfig.json │ ├── jest.config.js │ ├── jsdoc.json │ ├── package.json │ └── CHANGELOG.md ├── millicast-multiview-demo │ ├── multiview_app_img.png │ ├── .env.sample │ ├── env.js │ ├── vite.config.mjs │ ├── public │ │ └── css │ │ │ └── index.css │ ├── index.html │ ├── package.json │ ├── prepare.js │ ├── rollup.config.js │ └── README.md ├── millicast-publisher-demo │ ├── publisher_app_img.png │ ├── public │ │ ├── do-landscape-icon.png │ │ └── do-landscape-icon-wide.png │ ├── .env.sample │ ├── env.js │ ├── package.json │ ├── vite.config.mjs │ ├── src │ │ └── js │ │ │ ├── test │ │ │ ├── MillicastMedia.html │ │ │ ├── MillicastPublishUserMedia.html │ │ │ ├── MillicastMediaTest.js │ │ │ └── MillicastPublishUserMediaTest.js │ │ │ └── MillicastPublishUserMedia.js │ ├── prepare.js │ ├── rollup.config.js │ └── README.md ├── millicast-webaudio-delay-demo │ ├── delay_app_img.png │ ├── .env.sample │ ├── env.js │ ├── vite.config.mjs │ ├── public │ │ └── css │ │ │ └── index.css │ ├── package.json │ ├── prepare.js │ ├── rollup.config.js │ ├── README.md │ └── src │ │ └── viewer.js ├── millicast-chromecast-receiver │ ├── public │ │ ├── res │ │ │ ├── background-1.jpg │ │ │ └── logo_googleg_color_2x_web_48dp.png │ │ ├── index.html │ │ └── css │ │ │ └── index.css │ ├── .env.sample │ ├── README.md │ ├── vite.config.mjs │ ├── env.js │ ├── package.json │ ├── prepare.js │ ├── rollup.config.js │ └── src │ │ └── viewer.js └── millicast-viewer-demo │ ├── .env.sample │ ├── env.js │ ├── vite.config.mjs │ ├── package.json │ ├── prepare.js │ ├── rollup.config.js │ └── README.md ├── lerna.json ├── .github ├── ISSUE_TEMPLATE │ ├── questions.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── check-tests.yml │ ├── rc-release.yml │ ├── new-release.yml │ └── codeql-analysis.yml ├── .gitignore ├── check-signature.sh ├── .changeset ├── config.json └── README.md ├── .eslintrc.js ├── .eslintignore ├── nx.json └── package.json /img/tokens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millicast/millicast-sdk/HEAD/img/tokens.png -------------------------------------------------------------------------------- /packages/millicast-sdk/.npmignore: -------------------------------------------------------------------------------- 1 | tests 2 | docs 3 | coverage 4 | node_modules 5 | jest.config.js 6 | jsdoc.json 7 | docs-translations -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "npmClient": "npm", 6 | "useWorkspaces": true, 7 | "version": "0.1.40" 8 | } 9 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/samples/pic-1.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millicast/millicast-sdk/HEAD/packages/millicast-sdk/tests/unit/samples/pic-1.bin -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/samples/pic-2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millicast/millicast-sdk/HEAD/packages/millicast-sdk/tests/unit/samples/pic-2.bin -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/samples/pic-3.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millicast/millicast-sdk/HEAD/packages/millicast-sdk/tests/unit/samples/pic-3.bin -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/samples/pic-4.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millicast/millicast-sdk/HEAD/packages/millicast-sdk/tests/unit/samples/pic-4.bin -------------------------------------------------------------------------------- /packages/millicast-multiview-demo/multiview_app_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millicast/millicast-sdk/HEAD/packages/millicast-multiview-demo/multiview_app_img.png -------------------------------------------------------------------------------- /packages/millicast-publisher-demo/publisher_app_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millicast/millicast-sdk/HEAD/packages/millicast-publisher-demo/publisher_app_img.png -------------------------------------------------------------------------------- /packages/millicast-webaudio-delay-demo/delay_app_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millicast/millicast-sdk/HEAD/packages/millicast-webaudio-delay-demo/delay_app_img.png -------------------------------------------------------------------------------- /packages/millicast-publisher-demo/public/do-landscape-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millicast/millicast-sdk/HEAD/packages/millicast-publisher-demo/public/do-landscape-icon.png -------------------------------------------------------------------------------- /packages/millicast-chromecast-receiver/public/res/background-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millicast/millicast-sdk/HEAD/packages/millicast-chromecast-receiver/public/res/background-1.jpg -------------------------------------------------------------------------------- /packages/millicast-publisher-demo/public/do-landscape-icon-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millicast/millicast-sdk/HEAD/packages/millicast-publisher-demo/public/do-landscape-icon-wide.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/questions.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Questions 3 | about: Ask about the SDK and how to use it 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/millicast-multiview-demo/.env.sample: -------------------------------------------------------------------------------- 1 | # Create a .env file with the following variables: 2 | MILLICAST_ACCOUNT_ID=yourAccountId 3 | MILLICAST_STREAM_NAME=yourStreamName 4 | MILLICAST_DIRECTOR_ENDPOINT= -------------------------------------------------------------------------------- /packages/millicast-viewer-demo/.env.sample: -------------------------------------------------------------------------------- 1 | # Create a .env file with the following variables: 2 | MILLICAST_STREAM_NAME=yourStreamName 3 | MILLICAST_ACCOUNT_ID=yourAccountId 4 | MILLICAST_DIRECTOR_ENDPOINT= -------------------------------------------------------------------------------- /packages/millicast-chromecast-receiver/.env.sample: -------------------------------------------------------------------------------- 1 | # Create a .env file with the following variables: 2 | MILLICAST_ACCOUNT_ID=yourAccountId 3 | MILLICAST_STREAM_NAME=yourStreamName 4 | MILLICAST_DIRECTOR_ENDPOINT= -------------------------------------------------------------------------------- /packages/millicast-webaudio-delay-demo/.env.sample: -------------------------------------------------------------------------------- 1 | # Create a .env file with the following variables: 2 | MILLICAST_ACCOUNT_ID=yourAccountId 3 | MILLICAST_STREAM_NAME=yourStreamName 4 | MILLICAST_DIRECTOR_ENDPOINT= -------------------------------------------------------------------------------- /packages/millicast-chromecast-receiver/public/res/logo_googleg_color_2x_web_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millicast/millicast-sdk/HEAD/packages/millicast-chromecast-receiver/public/res/logo_googleg_color_2x_web_48dp.png -------------------------------------------------------------------------------- /packages/millicast-sdk/src/utils/FetchError.js: -------------------------------------------------------------------------------- 1 | export default class FetchError extends Error { 2 | constructor (message, status) { 3 | super(message) 4 | this.name = 'FetchError' 5 | this.status = status 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | coverage 4 | dist 5 | docs 6 | docs-translations 7 | .env 8 | puppeteerrc.cjs 9 | test-environment.js 10 | **/.DS_Store 11 | **/src/TransformWorker.worker.js 12 | vite.config.*.timestamp-* 13 | .nx/ -------------------------------------------------------------------------------- /packages/millicast-chromecast-receiver/README.md: -------------------------------------------------------------------------------- 1 | # Millicast Chromecast Receiver 2 | 3 | This application is deployed with application ID `B5B8307B`. 4 | 5 | You can refer to millicast-viewer-demo that implements Google Cast Sender to this application. -------------------------------------------------------------------------------- /packages/millicast-publisher-demo/.env.sample: -------------------------------------------------------------------------------- 1 | # Create a .env file with the following variables: 2 | MILLICAST_STREAM_NAME=yourStreamName 3 | MILLICAST_ACCOUNT_ID=yourAccountId 4 | MILLICAST_PUBLISH_TOKEN=yourPublishToken 5 | MILLICAST_DIRECTOR_ENDPOINT= -------------------------------------------------------------------------------- /check-signature.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # get the command output 4 | output=$(git verify-commit HEAD 2>&1) 5 | 6 | if [[ -z "$output" || $output == *"error:"* ]]; then 7 | echo "\033[0;31mError: Commit is not signed or verification failed.\033[0m" 8 | exit 1 9 | fi 10 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/e2e/utils/test-environment-sample.js: -------------------------------------------------------------------------------- 1 | // For use functional manual tests, create a test-enviroment.js with the following credentials. 2 | window.accountId = '' 3 | window.streamName = '' 4 | window.token = '' 5 | window.directorEndpoint = 'https://director.millicast.com' 6 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/SdpStereo.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to parse my SDP for set stereo so I can offer what I need to my peer 2 | 3 | Scenario: Set stereo 4 | Given a local sdp without stereo 5 | When I want to set stereo 6 | Then returns the sdp with stereo support -------------------------------------------------------------------------------- /packages/millicast-sdk/rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json' with { type: 'json' } 2 | import { dts } from 'rollup-plugin-dts' 3 | 4 | export default [ 5 | { 6 | input: './src/types/index.d.ts', 7 | output: [{ file: pkg.types, format: 'es' }], 8 | plugins: [dts()] 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /packages/millicast-sdk/vite.config.debug.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | export default defineConfig({ 4 | build: { 5 | sourcemap: 'inline', 6 | lib: { 7 | entry: 'src/index.js', 8 | name: 'millicast-debug', 9 | formats: ['umd'], 10 | fileName: 'millicast.debug' 11 | }, 12 | } 13 | }) -------------------------------------------------------------------------------- /packages/millicast-sdk/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | export default defineConfig({ 4 | build: { 5 | emptyOutDir: false, 6 | lib: { 7 | entry: 'src/index.js', 8 | name: 'millicast', 9 | formats: ['es', 'cjs', 'umd'], 10 | fileName: 'millicast' 11 | }, 12 | target: ['safari11','firefox66'] 13 | }, 14 | }) -------------------------------------------------------------------------------- /packages/millicast-sdk/jest_resolver.js: -------------------------------------------------------------------------------- 1 | module.exports = (request, options) => { 2 | // Remove any query parameters in the request path 3 | // (e.g. ?worker, which Vite uses for worker imports) 4 | if (request.includes('?')) { 5 | return options.defaultResolver(request.split('?')[0], options) 6 | } 7 | 8 | return options.defaultResolver(request, options) 9 | } 10 | -------------------------------------------------------------------------------- /packages/millicast-sdk/src/index.js: -------------------------------------------------------------------------------- 1 | import Logger from './Logger' 2 | import PeerConnection from './PeerConnection' 3 | import Signaling from './Signaling' 4 | import Director from './Director' 5 | import Publish from './Publish' 6 | import View from './View' 7 | export { 8 | Logger, 9 | PeerConnection, 10 | Signaling, 11 | Director, 12 | Publish, 13 | View 14 | } 15 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/SdpAbsCaptureTime.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to mungle my SDP for adding the absolute capture time header extension so I can offer what I need to my peer 2 | 3 | Scenario: Set abs-capture-time 4 | Given a local sdp without the header extension 5 | When I want to add header extension 6 | Then returns the sdp with the header extension -------------------------------------------------------------------------------- /packages/millicast-chromecast-receiver/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | export default defineConfig({ 4 | envPrefix: "MILLICAST_", 5 | build: { 6 | lib: { 7 | entry: "src/viewer.js", 8 | name: "viewer", 9 | fileName: "viewer", 10 | formats: ["umd"] 11 | } 12 | }, 13 | preview: { 14 | port: 10004 15 | } 16 | }) -------------------------------------------------------------------------------- /packages/millicast-sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "incremental": true, 5 | "allowJs": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "outDir": "dist", 9 | "strict": true, 10 | "skipLibCheck": false, 11 | }, 12 | "include": ["./src/**/*.ts"], 13 | "exclude": ["./node_modules/**/*"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/millicast-sdk/src/utils/StringUtils.js: -------------------------------------------------------------------------------- 1 | export function hexToUint8Array (hexString) { 2 | if (!hexString) { 3 | return new Uint8Array() 4 | } 5 | const length = hexString.length 6 | const uint8Array = new Uint8Array(length / 2) 7 | for (let i = 0; i < length; i += 2) { 8 | uint8Array[i / 2] = parseInt(hexString.substr(i, 2), 16) 9 | } 10 | return uint8Array 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | node: true, 5 | jest: true 6 | }, 7 | plugins: ['jest'], 8 | parserOptions: { 9 | ecmaFeatures: { 10 | experimentalObjectRestSpread: true 11 | }, 12 | ecmaVersion: 2020, 13 | sourceType: 'module' 14 | }, 15 | settings: { 16 | jest: { 17 | version: 24 18 | } 19 | }, 20 | extends: 'standard' 21 | } 22 | -------------------------------------------------------------------------------- /packages/millicast-sdk/src/utils/StreamTransform.js: -------------------------------------------------------------------------------- 1 | // Insertable streams for `MediaStreamTrack` is supported. 2 | export const supportsInsertableStreams = window.RTCRtpSender && 3 | !!RTCRtpSender.prototype.createEncodedStreams && 4 | window.RTCRtpReceiver && 5 | !!RTCRtpReceiver.prototype.createEncodedStreams 6 | 7 | // WebRTC RTP Script Transform is supported 8 | export const supportsRTCRtpScriptTransform = 'RTCRtpScriptTransform' in window 9 | -------------------------------------------------------------------------------- /packages/millicast-viewer-demo/env.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | 3 | const MILLICAST = /^MILLICAST_/i 4 | 5 | const getEnvironment = () => { 6 | dotenv.config() 7 | 8 | return Object.keys(process.env) 9 | .filter(key => MILLICAST.test(key)) 10 | .reduce( 11 | (env, key) => { 12 | env[key] = process.env[key] 13 | return env 14 | }, 15 | {} 16 | ) 17 | } 18 | 19 | export default getEnvironment 20 | -------------------------------------------------------------------------------- /packages/millicast-multiview-demo/env.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | 3 | const MILLICAST = /^MILLICAST_/i 4 | 5 | const getEnvironment = () => { 6 | dotenv.config() 7 | 8 | return Object.keys(process.env) 9 | .filter(key => MILLICAST.test(key)) 10 | .reduce( 11 | (env, key) => { 12 | env[key] = process.env[key] 13 | return env 14 | }, 15 | {} 16 | ) 17 | } 18 | 19 | export default getEnvironment 20 | -------------------------------------------------------------------------------- /packages/millicast-publisher-demo/env.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | 3 | const MILLICAST = /^MILLICAST_/i 4 | 5 | const getEnvironment = () => { 6 | dotenv.config() 7 | 8 | return Object.keys(process.env) 9 | .filter(key => MILLICAST.test(key)) 10 | .reduce( 11 | (env, key) => { 12 | env[key] = process.env[key] 13 | return env 14 | }, 15 | {} 16 | ) 17 | } 18 | 19 | export default getEnvironment 20 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/__mocks__/MockBrowser.js: -------------------------------------------------------------------------------- 1 | global.window = { navigator: { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } } 2 | 3 | export const changeBrowserMock = (browserAgent) => { 4 | Object.defineProperty(global.window.navigator, 'userAgent', { 5 | get: function () { 6 | return browserAgent 7 | }, 8 | configurable: true 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /packages/millicast-chromecast-receiver/env.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | 3 | const MILLICAST = /^MILLICAST_/i 4 | 5 | const getEnvironment = () => { 6 | dotenv.config() 7 | 8 | return Object.keys(process.env) 9 | .filter(key => MILLICAST.test(key)) 10 | .reduce( 11 | (env, key) => { 12 | env[key] = process.env[key] 13 | return env 14 | }, 15 | {} 16 | ) 17 | } 18 | 19 | export default getEnvironment 20 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/BaseWebRTC.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to get the peer so I can use it 2 | 3 | Scenario: Get existing RTC peer 4 | Given I have a BaseWebRTC instanced and existing peer 5 | When I want to get the peer 6 | Then returns the peer 7 | 8 | Scenario: Get no existing RTC peer 9 | Given I have a BaseWebRTC instanced and no existing peer 10 | When I want to get the peer 11 | Then returns null 12 | -------------------------------------------------------------------------------- /packages/millicast-webaudio-delay-demo/env.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | 3 | const MILLICAST = /^MILLICAST_/i 4 | 5 | const getEnvironment = () => { 6 | dotenv.config() 7 | 8 | return Object.keys(process.env) 9 | .filter(key => MILLICAST.test(key)) 10 | .reduce( 11 | (env, key) => { 12 | env[key] = process.env[key] 13 | return env 14 | }, 15 | {} 16 | ) 17 | } 18 | 19 | export default getEnvironment 20 | -------------------------------------------------------------------------------- /packages/millicast-sdk/src/utils/ObjectUtils.js: -------------------------------------------------------------------------------- 1 | export function swapPropertyValues (obj1, obj2, key) { 2 | // Check if both objects have the property 3 | // 4 | if (Object.prototype.hasOwnProperty.call(obj1, key) && 5 | Object.prototype.hasOwnProperty.call(obj2, key)) { 6 | const temp = obj1[key] 7 | obj1[key] = obj2[key] 8 | obj2[key] = temp 9 | } else { 10 | console.error(`One or both objects do not have the property "${key}"`) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/SdpRemoveLines.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to remove specific SDP lines so I can set the remote description in correct format to my peer 2 | 3 | Scenario: Remove existing line 4 | Given a local sdp 5 | When I want to remove an existing sdp line 6 | Then returns the sdp without the line 7 | 8 | Scenario: Remove unexisting line 9 | Given a local sdp 10 | When I want to remove an unexisting sdp line 11 | Then returns the sdp -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/SetRemoteDescription.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to set remote session description so I can start broadcasting or viewing a stream 2 | 3 | Scenario: Setting remote SDP to RTC peer 4 | Given I got the peer 5 | When I set the remote description 6 | Then the SDP is setted 7 | 8 | Scenario: Error setting remote SDP to RTC peer 9 | Given I got the peer 10 | When I set the remote description and peer returns an error 11 | Then throws an error -------------------------------------------------------------------------------- /packages/millicast-viewer-demo/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | export default defineConfig({ 4 | envPrefix: "MILLICAST_", 5 | server: { 6 | port: 10002, 7 | watch: { 8 | include: ['dist/**'], 9 | } 10 | }, 11 | build: { 12 | lib: { 13 | entry: "src/viewer.js", 14 | name: "viewer", 15 | fileName: (format) => `viewer.${format}.js`, 16 | formats: ["umd"] 17 | } 18 | }, 19 | preview: { 20 | port: 10002 21 | } 22 | }) -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/__mocks__/jwt-decode.js: -------------------------------------------------------------------------------- 1 | const jwtDecodeMock = jest.fn() 2 | 3 | const dummyToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJtaWxsaWNhc3QiOnt9fQ.IqT-PLLz-X7Wn7BNo-x4pFApAbMT9mmnlupR8eD9q4U' 4 | 5 | jwtDecodeMock.mockImplementation((token) => { 6 | return { 7 | millicast: { 8 | streamName: 'test-stream', 9 | jwt: dummyToken 10 | } 11 | } 12 | }) 13 | 14 | export default jwtDecodeMock 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .github 3 | /**/*.d.ts 4 | node_modules/** 5 | dist 6 | coverage/** 7 | docs 8 | packages/millicast-publisher-demo 9 | packages/millicast-viewer-demo 10 | packages/millicast-chromecast-receiver 11 | packages/millicast-multiview-demo 12 | /**/src/*.worker.js 13 | /**/rtc-drm-transform.js 14 | 15 | packages/millicast-sdk/rollup.config.js 16 | packages/millicast-sdk/src/Logger.js 17 | packages/millicast-sdk/src/utils/Diagnostics.js 18 | packages/millicast-sdk/tests/unit/LoggerDiagnose.steps.js 19 | -------------------------------------------------------------------------------- /packages/millicast-webaudio-delay-demo/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | export default defineConfig({ 4 | envPrefix: 'MILLICAST_', 5 | server: { 6 | port: 10003, 7 | watch: { 8 | include: ['dist/**'], 9 | } 10 | }, 11 | build: { 12 | lib: { 13 | entry: 'src/viewer.js', 14 | name: 'viewer', 15 | fileName: (format) => `viewer.${format}.js`, 16 | formats: ['umd'] 17 | } 18 | }, 19 | preview: { 20 | port: 10003 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /packages/millicast-multiview-demo/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | export default defineConfig({ 4 | envPrefix: "MILLICAST_", 5 | server: { 6 | port: 10005, 7 | watch: { 8 | include: ['dist/**'], 9 | } 10 | }, 11 | build: { 12 | lib: { 13 | entry: "src/multiviewer.js", 14 | name: "multiviewer", 15 | fileName: (format) => `multiviewer.${format}.js`, 16 | formats: ["umd"] 17 | } 18 | }, 19 | preview: { 20 | port: 10005 21 | } 22 | }) -------------------------------------------------------------------------------- /packages/millicast-sdk/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | module.exports = { 7 | testEnvironment: 'jsdom', 8 | clearMocks: true, 9 | coverageDirectory: 'coverage', 10 | preset: 'jest-puppeteer', 11 | transform: { 12 | '\\.[jt]sx?$': 'babel-jest' 13 | }, 14 | moduleNameMapper: { 15 | '^uuid$': require.resolve('uuid') 16 | }, 17 | resolver: './jest_resolver.js' 18 | } 19 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/FunctionalPublish.feature: -------------------------------------------------------------------------------- 1 | Feature: As a developer I want to publish a stream so i can ensure its working correctly 2 | 3 | Scenario Outline: Broadcasting stream 4 | Given a page with view options and a page with broadcaster options and codec 5 | When I broadcast a stream and connect to stream as viewer 6 | Then broadcast is active and Viewer receive video data 7 | 8 | Examples: 9 | | Codec | 10 | | h264 | 11 | | vp8 | 12 | | vp9 | 13 | | av1 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/Puppeteer.feature: -------------------------------------------------------------------------------- 1 | Feature: As a developer i want to run Jest with Puppeteer so i can test it 2 | 3 | Scenario: Load example page with Puppeteer 4 | Given i have a browser opened 5 | When i open a new page and go to the example web 6 | Then the web page title says "PuppeteerJest" 7 | 8 | Scenario: SDK loaded 9 | Given i have a browser opened and an example page with the Millicast SDK 10 | When i ask the "millicast" module 11 | Then returns an instance of "millicast" -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/__mocks__/Fetch.js: -------------------------------------------------------------------------------- 1 | global.fetch = jest.fn(() => 2 | Promise.resolve({ 3 | json: () => Promise.resolve({}) 4 | }) 5 | ) 6 | 7 | export function mockFetchJsonReturnValue (promiseImplementation) { 8 | global.fetch = jest.fn(() => 9 | Promise.resolve({ 10 | json: () => promiseImplementation 11 | }) 12 | ) 13 | } 14 | 15 | export function mockFetchRejectValue (promiseImplementation) { 16 | global.fetch = jest.fn(() => 17 | Promise.reject(promiseImplementation) 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/millicast-chromecast-receiver/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/ChangeMediaTrack.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to change a media track so I can change one of them while I'm broadcasting 2 | 3 | Scenario: Replace track to existing peer 4 | Given I have a peer connected 5 | When I want to change current audio track 6 | Then the track is changed 7 | 8 | Scenario: Replace track to unexisting peer 9 | Given I do not have a peer connected 10 | When I want to change the audio track 11 | Then the track is not changed 12 | 13 | Scenario: Replace unexisting track to peer 14 | Given I have a peer connected with video track 15 | When I want to change the audio track 16 | Then the track is not changed -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /packages/millicast-multiview-demo/public/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #e2e1e0; 3 | text-align: center; 4 | margin: 0px; 5 | padding: 0px; 6 | color: #555; 7 | font-family: 'Roboto'; 8 | } 9 | 10 | #remoteVideos { 11 | flex-wrap: wrap; 12 | margin: 1rem; 13 | } 14 | 15 | #mainVideo { 16 | margin: 1rem; 17 | } 18 | 19 | #mainVideo video { 20 | justify-content: center; 21 | width: 600px; 22 | height: 400px; 23 | cursor: pointer; 24 | } 25 | 26 | /* make video elements more small */ 27 | #remoteVideos video { 28 | width: 300px; 29 | height: 200px; 30 | margin: 1rem; 31 | cursor: pointer; 32 | } 33 | 34 | #dropDownSelector { 35 | display: block; 36 | } -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/SdpBitrate.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to parse my SDP for set bitrate so I can offer the bitrate that I need to my peer 2 | 3 | Scenario: Update bitrate with restrictions 4 | Given a local sdp 5 | When I want to update the bitrate to 1000 kbps 6 | Then returns the sdp with the bitrate updated 7 | 8 | Scenario: Update bitrate with no restrictions 9 | Given a local sdp with bitrate setted in 1000 kbps 10 | When I want to update the bitrate to unlimited 11 | Then returns the sdp with the bitrate updated 12 | 13 | Scenario: Update bitrate with restrictions in Firefox 14 | Given I am using Firefox and a local sdp 15 | When I want to update the bitrate to 1000 kbps 16 | Then returns the sdp with the bitrate updated -------------------------------------------------------------------------------- /packages/millicast-chromecast-receiver/public/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | --playback-logo-image: url('res/logo_googleg_color_2x_web_48dp.png'); 3 | } 4 | 5 | cast-media-player { 6 | --theme-hue: 180; 7 | --progress-color: rgb(0, 255, 255); 8 | --splash-image: url('res/background-1.jpg'); 9 | --splash-size: cover; 10 | } 11 | 12 | /* ------------------------------------------------- */ 13 | /* Sample Overlay Text */ 14 | /* ------------------------------------------------- */ 15 | cast-media-player:after { 16 | content: "SAMPLE"; 17 | position: absolute; 18 | left: 0; 19 | right: 0; 20 | top: 50%; 21 | bottom: 0; 22 | text-align: center; 23 | font-size: 200px; 24 | font-weight: bold; 25 | margin-top: -150px; 26 | opacity: 0.5; 27 | color: red; 28 | } -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/ManagePeerConnection.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to manage the peer connection so I can initialize a WebRTC connection 2 | 3 | Scenario: Get RTC peer without configuration 4 | Given I have no configuration 5 | When I get the RTC peer 6 | Then returns the peer 7 | 8 | Scenario: Get RTC peer without instance previously 9 | Given I have no configuration 10 | When I get the RTC peer without instance first 11 | Then returns null 12 | 13 | Scenario: Get RTC peer with configuration 14 | Given I have configuration 15 | When I get the RTC peer 16 | Then returns the peer 17 | 18 | Scenario: Close existing RTC peer 19 | Given I have a RTC peer 20 | When I close the RTC peer 21 | Then the peer is closed and emits connectionStateChange event -------------------------------------------------------------------------------- /packages/millicast-chromecast-receiver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "millicast-chromecast-receiver", 3 | "author": "Millicast, Inc.", 4 | "private": true, 5 | "scripts": { 6 | "prepare": "node prepare.js", 7 | "build": "vite build" 8 | }, 9 | "dependencies": { 10 | "@millicast/sdk": "file:../millicast-sdk" 11 | }, 12 | "devDependencies": { 13 | "@babel/core": "^7.23.6", 14 | "@babel/plugin-transform-runtime": "^7.13.10", 15 | "@babel/preset-env": "^7.23.6", 16 | "@rollup/plugin-babel": "^6.0.4", 17 | "@rollup/plugin-commonjs": "^28.0.3", 18 | "@rollup/plugin-node-resolve": "^16.0.1", 19 | "dotenv": "^16.5.0", 20 | "rollup": "4.43.0", 21 | "rollup-plugin-cleanup": "^3.2.1", 22 | "rollup-plugin-inject-process-env": "^1.3.1", 23 | "rollup-plugin-serve": "^3.0.0", 24 | "rollup-plugin-terser": "^7.0.2", 25 | "vite": "^6.3.5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/millicast-multiview-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Multiview Example

8 |
9 | 10 | 11 |
12 |
13 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: murillo128, R-Delfino95 7 | 8 | --- 9 | 10 | **Version affected** 11 | Please indicate the current version that you are using. 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Environment (please complete the following information):** 30 | - OS: [e.g. iOS] 31 | - Browser [e.g. chrome, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/GetPeerStatus.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to get the status so I can know the peer status 2 | 3 | Scenario: Get existing RTC peer status 4 | Given I have a peer instanced 5 | When I want to get the peer connection state 6 | Then returns the connection state 7 | 8 | Scenario: Get unexisting RTC peer status 9 | Given I do not have a peer connected 10 | When I want to get the peer connection state 11 | Then returns no value 12 | 13 | Scenario: Get connecting RTC peer status without connectionState 14 | Given I have a peer connecting without connectionState 15 | When I want to get the peer connection state 16 | Then returns the connection state 17 | 18 | Scenario: Get connected RTC peer status without connectionState 19 | Given I have a peer connected without connectionState 20 | When I want to get the peer connection state 21 | Then returns the connection state -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/LoggerHandlers.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to set a custom handler so I can resend logs to my own monitor system 2 | 3 | Scenario: Gets messages from same level 4 | Given I set a custom handler at INFO level 5 | When I log a message at INFO level 6 | Then I receive this message in handler 7 | 8 | Scenario: Gets messages from lower level 9 | Given I set a custom handler at INFO level 10 | When I log a message at DEBUG level 11 | Then custom handler does not receive any message 12 | 13 | Scenario: Gets messages from higher level 14 | Given I set a custom handler at INFO level 15 | When I log a message at ERROR level 16 | Then I receive this message in handler 17 | 18 | Scenario: Multiple handlers 19 | Given I set a custom handler at INFO level and other at ERROR level 20 | When I log a message at ERROR level 21 | Then both handlers receive this message 22 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/SdpSimulcast.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to set simulcast in my SDP so I can offer simulcast to my peer 2 | 3 | Scenario: Set simulcast in Chrome and h264 codec 4 | Given I am using Chrome, h264 codec and valid sdp 5 | When I want to set simulcast 6 | Then returns the sdp with simulcast updated 7 | 8 | Scenario: Set simulcast in Firefox and h264 codec 9 | Given I am using Firefox, h264 codec and valid sdp 10 | When I want to set simulcast 11 | Then returns the sdp without simulcast 12 | 13 | Scenario: Set simulcast in Chrome and vp9 codec 14 | Given I am using Chrome, vp9 codec and valid sdp 15 | When I want to set simulcast 16 | Then returns the sdp without simulcast 17 | 18 | Scenario: Set simulcast in Chrome, h264 codec and no video in sdp 19 | Given I am using Chrome, h264 codec and no video in sdp 20 | When I want to set simulcast 21 | Then throws an error -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/__mocks__/MockMediaStream.js: -------------------------------------------------------------------------------- 1 | export default class MockMediaStream { 2 | constructor (tracks = []) { 3 | this.audioTracks = [] 4 | this.videoTracks = [] 5 | for (const track of tracks) { 6 | this.addTrack(track) 7 | } 8 | } 9 | 10 | getAudioTracks () { 11 | return this.audioTracks 12 | } 13 | 14 | getVideoTracks () { 15 | return this.videoTracks 16 | } 17 | 18 | getTracks () { 19 | return this.audioTracks.concat(this.videoTracks) 20 | } 21 | 22 | addTrack (track) { 23 | const trackParsed = track 24 | trackParsed.getSettings = trackParsed.getSettings ?? getSettings 25 | if (track.kind === 'audio') { 26 | this.audioTracks.push(trackParsed) 27 | } else { 28 | this.videoTracks.push(trackParsed) 29 | } 30 | } 31 | } 32 | 33 | const getSettings = () => { 34 | return { 35 | channelCount: 0 36 | } 37 | } 38 | 39 | global.MediaStream = MockMediaStream 40 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/PeerStats.feature: -------------------------------------------------------------------------------- 1 | Feature: PeerConnectionStats 2 | 3 | Scenario: Initializes stats collection by default 4 | Given a PeerConnectionStats instance is created 5 | When no arguments are provided to the constructor 6 | Then the stats collection should be initialized 7 | 8 | Scenario: Does not initialize stats collection when disabled 9 | Given a PeerConnectionStats instance is created 10 | When the autoInitStats option is set to false 11 | Then the stats collection should not be initialized 12 | 13 | Scenario: Stops stats collection 14 | Given a PeerConnectionStats instance with a running collection 15 | When the stop method is called 16 | Then the stats collection should be stopped 17 | 18 | Scenario: Emits stats event when stats are received 19 | Given a PeerConnectionStats instance 20 | When the collection receives stats 21 | Then the "stats" event should be emitted with the received stats 22 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/LoggerLevels.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to log messages in different levels so I can group messages by type 2 | 3 | Scenario: Set global level to INFO 4 | Given global level is OFF 5 | When I set global level to INFO 6 | Then new level is INFO 7 | 8 | Scenario: Set global level to INFO with named logger 9 | Given global level is OFF and I have a named logger 10 | When I set global level to INFO 11 | Then global and named logger level are at INFO 12 | 13 | Scenario: Set level of named logger 14 | Given global level is OFF and I have a named logger 15 | When I set named logger level to INFO 16 | Then global level is OFF and named logger level is INFO 17 | 18 | Scenario: Get named logger already created 19 | Given I have a named logger 20 | When I get a named logger with same name 21 | Then returns the same named logger 22 | 23 | Scenario: Log message at logger level 24 | Given global level is INFO 25 | When I log a message at INFO 26 | Then a message is logged in console -------------------------------------------------------------------------------- /packages/millicast-webaudio-delay-demo/public/css/index.css: -------------------------------------------------------------------------------- 1 | 2 | body, html, button, input, select, textarea { 3 | font-family: "OpenSans", Helvetica, Verdana, Arial, sans-serif; 4 | } 5 | 6 | body { 7 | background-color: #fbfbfb; 8 | } 9 | 10 | html { 11 | overflow-x: hidden; 12 | } 13 | 14 | #main { 15 | position: absolute; 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | justify-content: center; 20 | background-color: #fbfbfb; 21 | top: 0; 22 | bottom: 0; 23 | left: 0; 24 | right: 0; 25 | } 26 | 27 | #slider { 28 | flex: none; 29 | background-color: white; 30 | overflow: hidden; 31 | width: 300px; 32 | height: 300px; 33 | margin: 10px; 34 | box-shadow: 0 2px 2px 0 rgb(0 0 0 / 14%), 0 3px 1px -2px rgb(0 0 0 / 20%), 0 1px 5px 0 rgb(0 0 0 / 12%); 35 | } 36 | 37 | #slider svg { 38 | fill: #eeeeee; 39 | padding: 20px; 40 | } 41 | 42 | #slider canvas { 43 | display: none; 44 | width: 300px; 45 | height: 300px; 46 | pointer-events: none; 47 | } 48 | 49 | #value { 50 | height: 40px; 51 | } 52 | 53 | #logo { 54 | position: fixed; 55 | z-index: 20; 56 | width: 120px; 57 | } 58 | -------------------------------------------------------------------------------- /packages/millicast-multiview-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "millicast-multiview-demo", 3 | "author": "Millicast, Inc.", 4 | "scripts": { 5 | "prepare": "node prepare.js", 6 | "build": "vite build", 7 | "preview": "vite preview", 8 | "start": "concurrently \"nodemon --watch dist --watch ../millicast-sdk --exec vite\" \"nodemon --watch src --watch ../millicast-sdk --exec vite build\"" 9 | }, 10 | "dependencies": { 11 | "@millicast/sdk": "file:../millicast-sdk" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.23.6", 15 | "@babel/plugin-transform-runtime": "^7.13.10", 16 | "@babel/preset-env": "^7.23.6", 17 | "@rollup/plugin-babel": "^6.0.4", 18 | "@rollup/plugin-commonjs": "^28.0.3", 19 | "@rollup/plugin-node-resolve": "^16.0.1", 20 | "concurrently": "^8.2.2", 21 | "dotenv": "^16.5.0", 22 | "nodemon": "^3.1.3", 23 | "rollup": "^4.43.0", 24 | "rollup-plugin-cleanup": "^3.2.1", 25 | "rollup-plugin-inject-process-env": "^1.3.1", 26 | "rollup-plugin-serve": "^3.0.0", 27 | "rollup-plugin-terser": "^7.0.2", 28 | "vite": "^6.3.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/millicast-publisher-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "millicast-publisher-demo", 3 | "author": "Millicast, Inc.", 4 | "private": true, 5 | "scripts": { 6 | "prepare": "node prepare.js", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "start": "concurrently \"nodemon --watch dist --watch ../millicast-sdk --exec vite\" \"nodemon --watch src --watch ../millicast-sdk --exec vite build\"" 10 | }, 11 | "dependencies": { 12 | "@millicast/sdk": "file:../millicast-sdk" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.13.10", 16 | "@babel/plugin-transform-runtime": "^7.13.10", 17 | "@babel/preset-env": "^7.13.12", 18 | "@rollup/plugin-babel": "^6.0.4", 19 | "@rollup/plugin-commonjs": "^28.0.3", 20 | "@rollup/plugin-node-resolve": "^16.0.1", 21 | "concurrently": "^8.2.2", 22 | "dotenv": "^16.5.0", 23 | "nodemon": "^3.1.3", 24 | "rollup": "^4.43.0", 25 | "rollup-plugin-cleanup": "^3.2.1", 26 | "rollup-plugin-inject-process-env": "^1.3.1", 27 | "rollup-plugin-serve": "^1.1.0", 28 | "rollup-plugin-terser": "^7.0.2", 29 | "vite": "^6.3.5" 30 | } 31 | } -------------------------------------------------------------------------------- /packages/millicast-viewer-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "millicast-viewer-demo", 3 | "author": "Millicast, Inc.", 4 | "private": true, 5 | "scripts": { 6 | "prepare": "node prepare.js", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "start": "concurrently \"nodemon --watch dist --watch ../millicast-sdk --exec vite\" \"nodemon --watch src --watch ../millicast-sdk --exec vite build\"" 10 | }, 11 | "dependencies": { 12 | "@millicast/sdk": "file:../millicast-sdk" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.13.10", 16 | "@babel/plugin-transform-runtime": "^7.13.10", 17 | "@babel/preset-env": "^7.13.12", 18 | "@rollup/plugin-babel": "^6.0.4", 19 | "@rollup/plugin-commonjs": "^28.0.3", 20 | "@rollup/plugin-node-resolve": "^16.0.1", 21 | "concurrently": "^8.2.2", 22 | "dotenv": "^16.5.0", 23 | "nodemon": "^3.1.3", 24 | "rollup": "^4.43.0", 25 | "rollup-plugin-cleanup": "^3.2.1", 26 | "rollup-plugin-inject-process-env": "^1.3.1", 27 | "rollup-plugin-serve": "^3.0.0", 28 | "rollup-plugin-terser": "^7.0.2", 29 | "vite": "^6.3.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/UpdateBitrateWebRTC.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to change max bitrate of a stream so I can adapt my stream to users with lower bandwidth 2 | 3 | Scenario: Update bitrate with restrictions 4 | Given I have a peer connected 5 | When I want to update the bitrate to 1000 kbps 6 | Then the bitrate is updated 7 | 8 | Scenario: Update bitrate with no restrictions 9 | Given I have a peer connected 10 | When I want to update the bitrate to unlimited 11 | Then the bitrate is updated 12 | 13 | Scenario: Update bitrate with restrictions in Firefox 14 | Given I am using Firefox and I have a peer connected 15 | When I want to update the bitrate to 1000 kbps 16 | Then the bitrate is updated 17 | 18 | Scenario: Update bitrate with no existing peer 19 | Given I do not have a peer connected 20 | When I want to update the bitrate to 1000 kbps 21 | Then throw no existing peer error 22 | 23 | Scenario: Check update bitrate throws exception when in Viewer mode 24 | Given I have a peer connected as a viewer 25 | When I want to update the bitrate to 1000 kbps 26 | Then I get an exception -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/__mocks__/MockRTCPeerConnectionNoConnectionState.js: -------------------------------------------------------------------------------- 1 | import MockRTCPeerConnection from './MockRTCPeerConnection' 2 | 3 | export const defaultConfig = { 4 | bundlePolicy: 'balanced', 5 | encodedInsertableStreams: false, 6 | iceCandidatePoolSize: 0, 7 | iceServers: [], 8 | iceTransportPolicy: 'all', 9 | rtcpMuxPolicy: 'require', 10 | sdpSemantics: 'unified-plan' 11 | } 12 | 13 | export default class MockRTCPeerConnectionNoConnectionState extends MockRTCPeerConnection { 14 | constructor (config = null) { 15 | super() 16 | this.connectionState = null 17 | this.iceConnectionState = 'new' 18 | } 19 | 20 | setRemoteDescription (answer) { 21 | this.currentRemoteDescription = answer 22 | this.iceConnectionState = 'connected' 23 | } 24 | 25 | onconnectionstatechange (state) {} 26 | 27 | oniceconnectionstatechange (state) { 28 | this.iceConnectionState = state 29 | } 30 | 31 | emitMockEvent (eventName, data) { 32 | if (eventName === 'ontrack') { 33 | this.ontrack(data) 34 | } else if (eventName === 'oniceconnectionstatechange') { 35 | this.oniceconnectionstatechange(data) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/PeerConnectionEvent.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to listen to peer connection events so I can take actions when they are fired 2 | 3 | Scenario: Receive new track from peer 4 | Given I have a peer connected 5 | When peer returns new track 6 | Then track event is fired 7 | 8 | Scenario: Get connecting status from peer 9 | Given I have a peer 10 | When peer starts to connect 11 | Then connectionStateChange event is fired 12 | 13 | Scenario: Get connected status from peer 14 | Given I have a peer 15 | When peer connects 16 | Then connectionStateChange event is fired 17 | 18 | Scenario: Get disconnected status from peer 19 | Given I have a peer connected 20 | When peer disconnects 21 | Then connectionStateChange event is fired 22 | 23 | Scenario: Get failed status from peer 24 | Given I have a peer connected 25 | When peer have a connection error 26 | Then connectionStateChange event is fired 27 | 28 | Scenario: Get new status from peer without connectionState 29 | Given I have a peer without connectionState 30 | When peer is instanced 31 | Then connectionStateChange event is fired -------------------------------------------------------------------------------- /packages/millicast-sdk/src/utils/BitStreamReader.js: -------------------------------------------------------------------------------- 1 | export default class BitStreamReader { 2 | constructor (uint8Array) { 3 | this.data = uint8Array 4 | this.bitOffset = 0 5 | } 6 | 7 | readBits (numBits) { 8 | if (this.bitOffset + numBits > this.data.length * 8) { 9 | throw new Error('Attempted to read past the end of the bitstream') 10 | } 11 | 12 | let value = 0 13 | for (let i = 0; i < numBits; i++) { 14 | const byteOffset = Math.floor(this.bitOffset / 8) 15 | const bitIndex = 7 - (this.bitOffset % 8) 16 | const bit = (this.data[byteOffset] >> bitIndex) & 1 17 | value |= bit << (numBits - 1 - i) 18 | this.bitOffset++ 19 | } 20 | return value 21 | } 22 | 23 | skip (numBits) { 24 | this.bitOffset += numBits 25 | } 26 | 27 | readExpGolombUnsigned () { 28 | let leadingZeros = -1 29 | for (let b = 0; b === 0; leadingZeros++) { 30 | b = this.readBits(1) 31 | } 32 | return (1 << leadingZeros) - 1 + this.readBits(leadingZeros) 33 | } 34 | 35 | readExpGolombSigned () { 36 | const value = this.readExpGolombUnsigned() 37 | return (value % 2 === 0) ? -(value / 2) : ((value + 1) / 2) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/millicast-webaudio-delay-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "millicast-webaudio-delay-demo", 3 | "author": "Millicast, Inc.", 4 | "private": true, 5 | "scripts": { 6 | "prepare": "node prepare.js", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "start": "concurrently \"nodemon --watch dist --watch ../millicast-sdk --exec vite\" \"nodemon --watch src --watch ../millicast-sdk --exec vite build\"" 10 | }, 11 | "dependencies": { 12 | "@maslick/radiaslider": "^1.9.8", 13 | "@millicast/sdk": "file:../millicast-sdk" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": "^7.23.6", 17 | "@babel/plugin-transform-runtime": "^7.13.10", 18 | "@babel/preset-env": "^7.23.6", 19 | "@rollup/plugin-babel": "^6.0.4", 20 | "@rollup/plugin-commonjs": "^28.0.3", 21 | "@rollup/plugin-node-resolve": "^16.0.1", 22 | "concurrently": "^8.2.2", 23 | "dotenv": "^16.5.0", 24 | "nodemon": "^3.1.3", 25 | "rollup": "4.43.0", 26 | "rollup-plugin-cleanup": "^3.2.1", 27 | "rollup-plugin-inject-process-env": "^1.3.1", 28 | "rollup-plugin-serve": "^3.0.0", 29 | "rollup-plugin-sourcemaps": "^0.6.3", 30 | "rollup-plugin-terser": "^7.0.2", 31 | "vite": "^6.3.5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/millicast-sdk/src/utils/UserAgent.js: -------------------------------------------------------------------------------- 1 | import UAParser from 'ua-parser-js' 2 | 3 | const chromeExcludedOS = ['iOS'] 4 | 5 | export default class UserAgent extends UAParser { 6 | constructor () { 7 | super(window.navigator.userAgent) 8 | } 9 | 10 | isChromium () { 11 | const browserData = this.getUA() 12 | 13 | return browserData.match(/Chrome/i) 14 | } 15 | 16 | isChrome () { 17 | const browserData = this.getBrowser() 18 | if (!browserData.name) { 19 | return false 20 | } 21 | const osData = this.getOS() 22 | 23 | let osAllowed = true 24 | const regex = new RegExp(chromeExcludedOS.join('|'), 'i') 25 | osAllowed = !regex.test(osData.name) 26 | 27 | return browserData.name.match(/Chrome/i) && osAllowed 28 | } 29 | 30 | isFirefox () { 31 | const browserData = this.getBrowser() 32 | if (!browserData.name) { 33 | return false 34 | } 35 | return browserData.name.match(/Firefox/i) 36 | } 37 | 38 | isOpera () { 39 | const browserData = this.getBrowser() 40 | if (!browserData.name) { 41 | return false 42 | } 43 | return browserData.name.match(/Opera/i) 44 | } 45 | 46 | isSafari () { 47 | const browserData = this.getBrowser() 48 | if (!browserData.name) { 49 | return false 50 | } 51 | return browserData.name.match(/Safari/i) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/millicast-publisher-demo/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | import { babel } from '@rollup/plugin-babel'; 6 | import cleanup from 'rollup-plugin-cleanup'; 7 | 8 | export default defineConfig({ 9 | envPrefix: "MILLICAST_", 10 | server: { 11 | port: 10001, 12 | watch: { 13 | include: ['dist/**'], 14 | } 15 | }, 16 | build: { 17 | lib: { 18 | entry: "src/publisher.js", 19 | name: "publisher", 20 | fileName: (format) => `publisher.${format}.js`, 21 | formats: ["umd"] 22 | }, 23 | rollupOptions: { 24 | plugins: [ 25 | nodeResolve({ preferBuiltins: false }), 26 | commonjs({ 27 | include: [/node_modules/, /src/], 28 | transformMixedEsModules: true 29 | }), 30 | babel({ 31 | babelHelpers: 'runtime', 32 | presets: [['@babel/preset-env', { targets: "defaults" }]], 33 | exclude: /node_modules/, 34 | plugins: ['@babel/plugin-transform-runtime'], 35 | compact: true 36 | }), 37 | terser(), 38 | cleanup({ 39 | comments: 'none', 40 | sourcemap: false 41 | }) 42 | ] 43 | } 44 | } 45 | }); -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/ManageSignaling.feature: -------------------------------------------------------------------------------- 1 | Feature: As a developer I want to manage signaling to Millicast Server so I can manage connection 2 | 3 | Scenario: Connect to existing server with no errors 4 | Given I have no previous connection to server 5 | When I want to connect to server 6 | Then returns the WebSocket connection and fires a connectionSuccess event 7 | 8 | Scenario: Connect again to existing server with no errors 9 | Given I have a previous connection to server 10 | When I want to connect to server 11 | Then returns the WebSocket connection and fires a connectionSuccess event 12 | 13 | Scenario: Connect to existing server with network errors 14 | Given I have no previous connection to server 15 | When I want to connect to no responding server 16 | Then fires a connectionError event 17 | 18 | Scenario: Receive broadcast events from server 19 | Given I am connected to server 20 | When the server send a broadcast event 21 | Then fires a broadcastEvent event 22 | 23 | Scenario: Close existing server connection 24 | Given I am connected to server 25 | When I want to close connection 26 | Then the connection closes 27 | 28 | Scenario: Close unexisting server connection 29 | Given I am not connected to server 30 | When I want to close connection 31 | Then websocket is not intitialized -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/SdpMultiopus.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to set multiopus in my SDP so I can offer surround to my peer 2 | 3 | Scenario: Set multiopus in Chrome and multichannel media stream 4 | Given I have a sdp and I am using Chrome 5 | When I want to set multiopus with multichannel media stream 6 | Then returns the sdp with multiopus updated 7 | 8 | Scenario: Set multiopus in Chrome and monochannel media stream 9 | Given I have a sdp and I am using Chrome 10 | When I want to set multiopus with monochannel media stream 11 | Then returns the sdp without multiopus 12 | 13 | Scenario: Set multiopus in Chrome and no media stream 14 | Given I have a sdp and I am using Chrome 15 | When I want to set multiopus with no media stream 16 | Then returns the sdp with multiopus updated 17 | 18 | Scenario: Set multiopus again in Chrome 19 | Given I have a sdp with multiopus I am using Chrome 20 | When I want to set multiopus 21 | Then returns the same sdp 22 | 23 | Scenario: Set multiopus in iOS Chrome 24 | Given I have a sdp and I am using iOS Chrome 25 | When I want to set multiopus 26 | Then returns the sdp with multiopus updated 27 | 28 | Scenario: Set multiopus in Firefox 29 | Given I have a sdp and I am using Firefox 30 | When I want to set multiopus 31 | Then returns the sdp without multiopus -------------------------------------------------------------------------------- /packages/millicast-sdk/jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": false 4 | }, 5 | "source": { 6 | "include": [ 7 | "src" 8 | ], 9 | "includePattern": ".+\\.js(doc|x)?$", 10 | "excludePattern": "(^|\\/|\\\\)_" 11 | }, 12 | "plugins": [ 13 | "plugins/markdown", 14 | "../../node_modules/jsdoc-export-default-interop/dist/index", 15 | "../../node_modules/jsdoc-i18n-plugin/index.js" 16 | ], 17 | "markdown": { 18 | "idInHeadings": true 19 | }, 20 | "i18n": { 21 | "locale": "en", 22 | "directory": "docs-translations/", 23 | "srcDir": "src/", 24 | "extension": ".json" 25 | }, 26 | "opts": { 27 | "template": "../../node_modules/clean-jsdoc-theme", 28 | "encoding": "utf8", 29 | "destination": "docs/", 30 | "recurse": true, 31 | "verbose": true, 32 | "theme_opts": { 33 | "title": "Millicast SDK", 34 | "theme": "light", 35 | "menu": [ 36 | { 37 | "title": "Github", 38 | "link": "https://github.com/millicast/millicast-sdk", 39 | "target": "_blank" 40 | }, 41 | { 42 | "title": "NPM Package", 43 | "link": "https://www.npmjs.com/package/@millicast/sdk", 44 | "target": "_blank" 45 | } 46 | ] 47 | } 48 | }, 49 | "templates": { 50 | "cleverLinks": false, 51 | "monospaceLinks": false 52 | } 53 | } -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/LoggerDiagnose.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to get relevant information of the connection to the server 2 | 3 | Scenario: Get information with failed connection 4 | Given connection has failed 5 | When I call Logger diagnose function 6 | Then console logs an information object 7 | 8 | Scenario: Get information while viewing a stream 9 | Given connection to a stream 10 | When I call Logger diagnose function 11 | Then console logs an information object 12 | 13 | Scenario: Get information while viewing a stream with stats 14 | Given connection to a stream and stats enabled 15 | When I call Logger diagnose function 16 | Then console logs an information object with stats attribute not empty 17 | 18 | Scenario: Get information while publishing a stream 19 | Given a stream being published 20 | When I call Logger diagnose function 21 | Then console logs an information object 22 | 23 | Scenario: Get information while failing to publish a stream 24 | Given a stream cannot be published 25 | When I call Logger diagnose function 26 | Then console logs an information object 27 | 28 | Scenario: Get information in another browser 29 | Given I am in Firefox and start a connection to a stream 30 | When I call Logger diagnose function 31 | Then console logs an information object with Firefox's userAgent 32 | -------------------------------------------------------------------------------- /packages/millicast-publisher-demo/src/js/test/MillicastMedia.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 9 |
10 |
11 | Microphone (audio input): 12 | 17 | Speakers (audio output): 18 | 19 | Video: 20 | 25 |
26 | 27 |
28 | 29 | 30 | 31 | 34 | 37 | 38 | 39 |
40 |
41 | -------------------------------------------------------------------------------- /.github/workflows/check-tests.yml: -------------------------------------------------------------------------------- 1 | name: Check tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | workflow_dispatch: 13 | 14 | jobs: 15 | eslint: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Install the dependencies 🧱 20 | run: npm ci 21 | - name: Check Linter and Build 📑 22 | run: | 23 | npm run build 24 | npx eslint . --ext .js,.jsx,.ts,.tsx 25 | 26 | unit-test: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: Install the dependencies 🧱 31 | run: npm ci 32 | - name: Unit Testing 🧪 33 | working-directory: './packages/millicast-sdk' 34 | run: npm run test-unit 35 | 36 | e2e-test: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v4 40 | - name: Install the dependencies 🧱 41 | run: npm ci 42 | - name: End-2-End Testing 43 | working-directory: './packages/millicast-sdk' 44 | run: npm run test-e2e 45 | env: 46 | ACCOUNT_ID: ${{vars.PUBLISHER_DEMO_ACC_ID}} 47 | PUBLISH_TOKEN: ${{secrets.PUBLISHER_DEMO_TOKEN}} 48 | 49 | build-docs: 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v4 53 | - name: Install the dependencies 🧱 54 | run: npm ci 55 | - name: build docs 56 | run: npm run build-docs -------------------------------------------------------------------------------- /packages/millicast-chromecast-receiver/prepare.js: -------------------------------------------------------------------------------- 1 | // This script is equals to: "rm -R dist && mkdir dist && cp -R public/* dist/" 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const directoryTarget = 'dist' 6 | const directorySource = 'public' 7 | 8 | function removeDirectory (target) { 9 | if (fs.existsSync(target)) { 10 | fs.rmdirSync(target, { recursive: true }) 11 | } 12 | } 13 | 14 | function copyFileSync (source, target) { 15 | let targetFile = target 16 | if (fs.existsSync(target)) { 17 | if (fs.lstatSync(target).isDirectory()) { 18 | targetFile = path.join(target, path.basename(source)) 19 | } 20 | } 21 | fs.writeFileSync(targetFile, fs.readFileSync(source)) 22 | } 23 | 24 | function createDirectoryIfNotExists (directory) { 25 | if (!fs.existsSync(directory)) { fs.mkdirSync(directory) } 26 | } 27 | 28 | function copyFolderRecursiveSync (source, target) { 29 | createDirectoryIfNotExists(target) 30 | 31 | if (fs.lstatSync(source).isDirectory()) { 32 | const files = fs.readdirSync(source) 33 | files.forEach(function (file) { 34 | const curSource = path.join(source, file) 35 | if (fs.lstatSync(curSource).isDirectory()) { 36 | const curTarget = path.join(target, file) 37 | copyFolderRecursiveSync(curSource, curTarget) 38 | } else { 39 | copyFileSync(curSource, target) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | removeDirectory(directoryTarget) 46 | copyFolderRecursiveSync(directorySource, directoryTarget) 47 | -------------------------------------------------------------------------------- /packages/millicast-webaudio-delay-demo/prepare.js: -------------------------------------------------------------------------------- 1 | // This script is equals to: "rm -R dist && mkdir dist && cp -R public/* dist/" 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const directoryTarget = 'dist' 6 | const directorySource = 'public' 7 | 8 | function removeDirectory (target) { 9 | if (fs.existsSync(target)) { 10 | fs.rmdirSync(target, { recursive: true }) 11 | } 12 | } 13 | 14 | function copyFileSync (source, target) { 15 | let targetFile = target 16 | if (fs.existsSync(target)) { 17 | if (fs.lstatSync(target).isDirectory()) { 18 | targetFile = path.join(target, path.basename(source)) 19 | } 20 | } 21 | fs.writeFileSync(targetFile, fs.readFileSync(source)) 22 | } 23 | 24 | function createDirectoryIfNotExists (directory) { 25 | if (!fs.existsSync(directory)) { fs.mkdirSync(directory) } 26 | } 27 | 28 | function copyFolderRecursiveSync (source, target) { 29 | createDirectoryIfNotExists(target) 30 | 31 | if (fs.lstatSync(source).isDirectory()) { 32 | const files = fs.readdirSync(source) 33 | files.forEach(function (file) { 34 | const curSource = path.join(source, file) 35 | if (fs.lstatSync(curSource).isDirectory()) { 36 | const curTarget = path.join(target, file) 37 | copyFolderRecursiveSync(curSource, curTarget) 38 | } else { 39 | copyFileSync(curSource, target) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | removeDirectory(directoryTarget) 46 | copyFolderRecursiveSync(directorySource, directoryTarget) 47 | -------------------------------------------------------------------------------- /packages/millicast-multiview-demo/prepare.js: -------------------------------------------------------------------------------- 1 | //This script is equals to: "rm -R dist && mkdir dist && cp -R public/* dist/" 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const directoryTarget = 'dist' 6 | const directorySource = 'public' 7 | 8 | function removeDirectory(target){ 9 | if (fs.existsSync(target)){ 10 | fs.rmdirSync(target, {recursive: true}) 11 | } 12 | } 13 | 14 | function copyFileSync(source, target){ 15 | let targetFile = target 16 | if (fs.existsSync(target)){ 17 | if (fs.lstatSync(target).isDirectory()){ 18 | targetFile = path.join(target, path.basename(source)) 19 | } 20 | } 21 | fs.writeFileSync(targetFile, fs.readFileSync(source)) 22 | } 23 | 24 | function createDirectoryIfNotExists(directory){ 25 | if(!fs.existsSync(directory)) 26 | fs.mkdirSync(directory) 27 | } 28 | 29 | function copyFolderRecursiveSync(source, target){ 30 | createDirectoryIfNotExists(target) 31 | 32 | if(fs.lstatSync(source).isDirectory()){ 33 | const files = fs.readdirSync(source) 34 | files.forEach(function(file) { 35 | const curSource = path.join(source, file) 36 | if (fs.lstatSync(curSource).isDirectory()){ 37 | const curTarget = path.join(target, file) 38 | copyFolderRecursiveSync(curSource, curTarget) 39 | } 40 | else{ 41 | copyFileSync(curSource, target) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | 48 | removeDirectory(directoryTarget) 49 | copyFolderRecursiveSync(directorySource, directoryTarget) -------------------------------------------------------------------------------- /packages/millicast-publisher-demo/prepare.js: -------------------------------------------------------------------------------- 1 | //This script is equals to: "rm -R dist && mkdir dist && cp -R public/* dist/" 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const directoryTarget = 'dist' 6 | const directorySource = 'public' 7 | 8 | function removeDirectory(target){ 9 | if (fs.existsSync(target)){ 10 | fs.rmdirSync(target, {recursive: true}) 11 | } 12 | } 13 | 14 | function copyFileSync(source, target){ 15 | let targetFile = target 16 | if (fs.existsSync(target)){ 17 | if (fs.lstatSync(target).isDirectory()){ 18 | targetFile = path.join(target, path.basename(source)) 19 | } 20 | } 21 | fs.writeFileSync(targetFile, fs.readFileSync(source)) 22 | } 23 | 24 | function createDirectoryIfNotExists(directory){ 25 | if(!fs.existsSync(directory)) 26 | fs.mkdirSync(directory) 27 | } 28 | 29 | function copyFolderRecursiveSync(source, target){ 30 | createDirectoryIfNotExists(target) 31 | 32 | if(fs.lstatSync(source).isDirectory()){ 33 | const files = fs.readdirSync(source) 34 | files.forEach(function(file) { 35 | const curSource = path.join(source, file) 36 | if (fs.lstatSync(curSource).isDirectory()){ 37 | const curTarget = path.join(target, file) 38 | copyFolderRecursiveSync(curSource, curTarget) 39 | } 40 | else{ 41 | copyFileSync(curSource, target) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | 48 | removeDirectory(directoryTarget) 49 | copyFolderRecursiveSync(directorySource, directoryTarget) -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/LoggerHistory.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to get log history of my session so I can get details from my current session 2 | 3 | Scenario: Get history with logger turned OFF 4 | Given I set logger level at OFF 5 | When I log a message at INFO level 6 | Then I get this message from history 7 | 8 | Scenario: Get history when log 5 messages 9 | Given I have no previous logs and history max size is 5 10 | When I log 5 messages at INFO level 11 | Then I get those messages from history 12 | 13 | Scenario: Get history when log more than 5 messages 14 | Given I have no previous logs and history max size is 5 15 | When I log 6 messages at INFO level 16 | Then I get the last 5 messages from history 17 | 18 | Scenario: Disable history with no previous logs 19 | Given I have no previous logs and history max size is 0 20 | When I log a message at INFO level 21 | Then log history is empty 22 | 23 | Scenario: Disable history with previous logs 24 | Given I have one previous log message and history max size is 5 25 | When I set history max size to 0 and I log a message at INFO level 26 | Then log history is empty 27 | 28 | Scenario: Change max size after logs some messages 29 | Given I have 5 previous log messages and history max size is 5 30 | When I set history max size to 6 and I log a message at ERROR level 31 | Then I get all log messages from history 32 | 33 | Scenario: Get current max history size 34 | Given history max size is 5 35 | When I get current history max size 36 | Then returns 5 37 | -------------------------------------------------------------------------------- /packages/millicast-viewer-demo/prepare.js: -------------------------------------------------------------------------------- 1 | //This script is equals to: "rm -R dist && mkdir dist && cp -R public/* dist/" 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const directoryTarget = 'dist' 6 | const directorySource = 'public' 7 | 8 | function removeDirectory(target){ 9 | if (fs.existsSync(target)){ 10 | fs.rmdirSync(target, {recursive: true}) 11 | } 12 | } 13 | 14 | function copyFileSync(source, target){ 15 | let targetFile = target 16 | if (fs.existsSync(target)){ 17 | if (fs.lstatSync(target).isDirectory()){ 18 | targetFile = path.join(target, path.basename(source)) 19 | } 20 | } 21 | fs.writeFileSync(targetFile, fs.readFileSync(source)) 22 | } 23 | 24 | function createDirectoryIfNotExists(directory){ 25 | if(!fs.existsSync(directory)) 26 | fs.mkdirSync(directory) 27 | } 28 | 29 | function copyFolderRecursiveSync(source, target){ 30 | createDirectoryIfNotExists(target) 31 | 32 | if(fs.lstatSync(source).isDirectory()){ 33 | const files = fs.readdirSync(source) 34 | files.forEach(function(file) { 35 | const curSource = path.join(source, file) 36 | if (fs.lstatSync(curSource).isDirectory()){ 37 | const curTarget = path.join(target, file) 38 | copyFolderRecursiveSync(curSource, curTarget) 39 | } 40 | else{ 41 | copyFileSync(curSource, target) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | 48 | removeDirectory(directoryTarget) 49 | copyFolderRecursiveSync(directorySource, directoryTarget) -------------------------------------------------------------------------------- /packages/millicast-viewer-demo/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import injectProcessEnv from 'rollup-plugin-inject-process-env' 4 | import { terser } from 'rollup-plugin-terser' 5 | import { babel } from '@rollup/plugin-babel' 6 | import cleanup from 'rollup-plugin-cleanup' 7 | import serve from 'rollup-plugin-serve' 8 | 9 | import getEnvironment from './env' 10 | 11 | const environment = getEnvironment() 12 | 13 | let watchPlugins = [] 14 | if (process.env.ROLLUP_WATCH) { 15 | watchPlugins = [ 16 | serve({ 17 | open: true, 18 | contentBase: 'dist', 19 | port: 10002 20 | }) 21 | ] 22 | } 23 | 24 | export default [ 25 | { 26 | input: 'src/viewer.js', 27 | output: { 28 | name: 'viewer', 29 | file: 'dist/viewer.umd.js', 30 | format: 'umd', 31 | globals: { 32 | 'millicast-sdk': 'millicastSdkJs' 33 | } 34 | }, 35 | plugins: [ 36 | nodeResolve({ preferBuiltins: false }), 37 | commonjs({ 38 | include: [/node_modules/, /src/], 39 | transformMixedEsModules: true 40 | }), 41 | injectProcessEnv({ 42 | ...environment 43 | }), 44 | babel({ 45 | babelHelpers: 'runtime', 46 | presets: ['@babel/preset-env'], 47 | exclude: ['/node_modules/**'], 48 | plugins: ['@babel/plugin-transform-runtime'] 49 | }), 50 | terser(), 51 | cleanup({ 52 | comments: 'none', 53 | sourcemap: false 54 | }), 55 | ...watchPlugins 56 | ] 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /packages/millicast-chromecast-receiver/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import injectProcessEnv from 'rollup-plugin-inject-process-env' 4 | import { terser } from 'rollup-plugin-terser' 5 | import { babel } from '@rollup/plugin-babel' 6 | import cleanup from 'rollup-plugin-cleanup' 7 | import serve from 'rollup-plugin-serve' 8 | 9 | import getEnvironment from './env' 10 | 11 | const environment = getEnvironment() 12 | 13 | let watchPlugins = [] 14 | if (process.env.ROLLUP_WATCH) { 15 | watchPlugins = [ 16 | serve({ 17 | open: true, 18 | contentBase: 'dist', 19 | port: 10004 20 | }) 21 | ] 22 | } 23 | 24 | export default [ 25 | { 26 | input: 'src/viewer.js', 27 | output: { 28 | name: 'viewer', 29 | file: 'dist/viewer.umd.js', 30 | format: 'umd', 31 | globals: { 32 | 'millicast-sdk': 'millicastSdkJs' 33 | } 34 | }, 35 | plugins: [ 36 | nodeResolve({ preferBuiltins: false }), 37 | commonjs({ 38 | include: [/node_modules/, /src/], 39 | transformMixedEsModules: true 40 | }), 41 | injectProcessEnv({ 42 | ...environment 43 | }), 44 | babel({ 45 | babelHelpers: 'runtime', 46 | presets: ['@babel/preset-env'], 47 | exclude: ['/node_modules/**'], 48 | plugins: ['@babel/plugin-transform-runtime'] 49 | }), 50 | terser(), 51 | cleanup({ 52 | comments: 'none', 53 | sourcemap: false 54 | }), 55 | ...watchPlugins 56 | ] 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /packages/millicast-publisher-demo/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import injectProcessEnv from 'rollup-plugin-inject-process-env' 4 | import { terser } from 'rollup-plugin-terser' 5 | import { babel } from '@rollup/plugin-babel' 6 | import cleanup from 'rollup-plugin-cleanup' 7 | import serve from 'rollup-plugin-serve' 8 | 9 | import getEnvironment from './env' 10 | 11 | const environment = getEnvironment() 12 | 13 | let watchPlugins = [] 14 | if (process.env.ROLLUP_WATCH) { 15 | watchPlugins = [ 16 | serve({ 17 | open: true, 18 | contentBase: 'dist', 19 | port: 10001 20 | }) 21 | ] 22 | } 23 | 24 | export default [ 25 | { 26 | input: 'src/publisher.js', 27 | output: { 28 | name: 'publisher', 29 | file: 'dist/publisher.umd.js', 30 | format: 'umd', 31 | globals: { 32 | 'millicast-sdk': 'millicastSdkJs' 33 | } 34 | }, 35 | plugins: [ 36 | nodeResolve({ preferBuiltins: false }), 37 | commonjs({ 38 | include: [/node_modules/, /src/], 39 | transformMixedEsModules: true 40 | }), 41 | injectProcessEnv({ 42 | ...environment 43 | }), 44 | babel({ 45 | babelHelpers: 'runtime', 46 | presets: ['@babel/preset-env'], 47 | exclude: ['/node_modules/**'], 48 | plugins: ['@babel/plugin-transform-runtime'] 49 | }), 50 | terser(), 51 | cleanup({ 52 | comments: 'none', 53 | sourcemap: false 54 | }), 55 | ...watchPlugins 56 | ] 57 | } 58 | ] -------------------------------------------------------------------------------- /packages/millicast-multiview-demo/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import injectProcessEnv from 'rollup-plugin-inject-process-env' 4 | import { terser } from 'rollup-plugin-terser' 5 | import { babel } from '@rollup/plugin-babel' 6 | import cleanup from 'rollup-plugin-cleanup' 7 | import serve from 'rollup-plugin-serve' 8 | 9 | import getEnvironment from './env' 10 | 11 | const environment = getEnvironment() 12 | 13 | let watchPlugins = [] 14 | if (process.env.ROLLUP_WATCH) { 15 | watchPlugins = [ 16 | serve({ 17 | open: true, 18 | contentBase: 'dist', 19 | port: 10005 20 | }) 21 | ] 22 | } 23 | 24 | export default [ 25 | { 26 | input: 'src/multiviewer.js', 27 | output: { 28 | name: 'multiviewer', 29 | file: 'dist/multiviewer.umd.js', 30 | format: 'umd', 31 | globals: { 32 | 'millicast-sdk': 'millicastSdkJs' 33 | } 34 | }, 35 | plugins: [ 36 | nodeResolve({ preferBuiltins: false }), 37 | commonjs({ 38 | include: [/node_modules/, /src/], 39 | transformMixedEsModules: true 40 | }), 41 | injectProcessEnv({ 42 | ...environment 43 | }), 44 | babel({ 45 | babelHelpers: 'runtime', 46 | presets: ['@babel/preset-env'], 47 | exclude: ['/node_modules/**'], 48 | plugins: ['@babel/plugin-transform-runtime'] 49 | }), 50 | terser(), 51 | cleanup({ 52 | comments: 'none', 53 | sourcemap: false 54 | }), 55 | ...watchPlugins 56 | ] 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /.github/workflows/rc-release.yml: -------------------------------------------------------------------------------- 1 | name: Release Candidate 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'next-*' 9 | 10 | jobs: 11 | build-sdk: 12 | name: Build WebSDK 13 | runs-on: ubuntu-latest 14 | if: ${{ startsWith(github.ref, 'refs/tags/next-') }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Install the dependencies 🧱 18 | run: npm ci 19 | - name: build 20 | run: npx lerna run build --scope=@millicast/sdk 21 | - name: Upload SDK build artifact 22 | uses: actions/upload-artifact@v4 23 | with: 24 | name: sdk-dist 25 | path: packages/millicast-sdk/dist 26 | pulish-next: 27 | name: Publish SDK to NPM for next- tag 28 | runs-on: ubuntu-latest 29 | needs: build-sdk 30 | if: ${{ startsWith(github.ref, 'refs/tags/next-') }} 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Install the dependencies 🧱 34 | run: npm ci 35 | - name: Download SDK build artifact 36 | uses: actions/download-artifact@v4 37 | with: 38 | name: sdk-dist 39 | path: packages/millicast-sdk/dist 40 | - name: Add Readme to package 41 | run: cp README.md packages/millicast-sdk/README.md 42 | - name: Login to npm 43 | run: | 44 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc 45 | - name: publish 46 | working-directory: './packages/millicast-sdk' 47 | env: 48 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 49 | run: | 50 | npm publish --tag next -------------------------------------------------------------------------------- /packages/millicast-webaudio-delay-demo/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import injectProcessEnv from 'rollup-plugin-inject-process-env' 4 | import { terser } from 'rollup-plugin-terser' 5 | import { babel } from '@rollup/plugin-babel' 6 | import cleanup from 'rollup-plugin-cleanup' 7 | import serve from 'rollup-plugin-serve' 8 | 9 | import getEnvironment from './env' 10 | 11 | const environment = getEnvironment() 12 | 13 | let watchPlugins = [] 14 | if (process.env.ROLLUP_WATCH) { 15 | watchPlugins = [ 16 | serve({ 17 | open: true, 18 | contentBase: 'dist', 19 | port: 10003 20 | }) 21 | ] 22 | } 23 | 24 | export default [ 25 | { 26 | input: 'src/viewer.js', 27 | output: { 28 | name: 'viewer', 29 | file: 'dist/viewer.umd.js', 30 | format: 'umd', 31 | globals: { 32 | 'millicast-sdk': 'millicastSdkJs' 33 | } 34 | }, 35 | plugins: [ 36 | nodeResolve({ preferBuiltins: false }), 37 | commonjs({ 38 | include: [/node_modules/, /src/], 39 | transformMixedEsModules: true 40 | }), 41 | injectProcessEnv({ 42 | ...environment 43 | }), 44 | babel({ 45 | babelHelpers: 'runtime', 46 | presets: ['@babel/preset-env'], 47 | exclude: ['/node_modules/**'], 48 | plugins: ['@babel/plugin-transform-runtime'], 49 | inputSourceMap: false 50 | }), 51 | terser(), 52 | cleanup({ 53 | comments: 'none', 54 | sourcemap: false 55 | }), 56 | ...watchPlugins 57 | ] 58 | } 59 | ] 60 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/BaseWebRTC.steps.js: -------------------------------------------------------------------------------- 1 | import { loadFeature, defineFeature } from 'jest-cucumber' 2 | import BaseWebRTC from '../../src/utils/BaseWebRTC' 3 | import { defaultConfig } from './__mocks__/MockRTCPeerConnection' 4 | import './__mocks__/MockMediaStream' 5 | const feature = loadFeature('../features/BaseWebRTC.feature', { loadRelativePath: true, errors: true }) 6 | 7 | defineFeature(feature, test => { 8 | afterEach(async () => { 9 | jest.restoreAllMocks() 10 | }) 11 | 12 | test('Get existing RTC peer', ({ given, when, then }) => { 13 | let baseWebRTC = null 14 | let peer = null 15 | 16 | given('I have a BaseWebRTC instanced and existing peer', async () => { 17 | baseWebRTC = new BaseWebRTC('test', () => {}, null, false) 18 | await baseWebRTC.webRTCPeer.createRTCPeer() 19 | }) 20 | 21 | when('I want to get the peer', () => { 22 | peer = baseWebRTC.getRTCPeerConnection() 23 | }) 24 | 25 | then('returns the peer', async () => { 26 | expect(peer.getConfiguration()).toMatchObject({ ...defaultConfig, bundlePolicy: 'balanced' }) 27 | }) 28 | }) 29 | 30 | test('Get no existing RTC peer', ({ given, when, then }) => { 31 | let baseWebRTC = null 32 | let peer = null 33 | 34 | given('I have a BaseWebRTC instanced and no existing peer', async () => { 35 | baseWebRTC = new BaseWebRTC('test', () => {}, null, false) 36 | baseWebRTC.webRTCPeer = null 37 | }) 38 | 39 | when('I want to get the peer', () => { 40 | peer = baseWebRTC.getRTCPeerConnection() 41 | }) 42 | 43 | then('returns null', async () => { 44 | expect(peer).toBeNull() 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/OfferSubscribingStream.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to signal Millicast Server so I can offer subscribing a stream 2 | 3 | Scenario: Offer a SDP with no previous connection 4 | Given a local sdp and no previous connection to server 5 | When I offer my local sdp 6 | Then returns a filtered sdp to offer to remote peer 7 | 8 | Scenario: Offer a SDP with no previous connection and browser does not support getCapabilities 9 | Given a local sdp and no previous connection to server 10 | When I offer my local sdp 11 | Then returns a filtered sdp to offer to remote peer 12 | 13 | Scenario: Offer a SDP with no previous connection and options as object 14 | Given a local sdp and no previous connection to server 15 | When I offer my local sdp using options object 16 | Then returns a filtered sdp to offer to remote peer 17 | 18 | Scenario: Offer a SDP with previous connection 19 | Given a local sdp and a previous active connection to server 20 | When I offer my local spd 21 | Then returns a filtered sdp to offer to remote peer 22 | 23 | Scenario: Offer no SDP with no previous connection 24 | Given I have not previous connection to server 25 | When I offer a null sdp 26 | Then throws no sdp error 27 | 28 | Scenario: Offer no SDP with previous connection 29 | Given I have previous connection to server 30 | When I offer a null sdp 31 | Then throws no sdp error 32 | 33 | Scenario: Offer a SDP with unexistent stream name 34 | Given I have not previous connection to server 35 | When I offer my local spd and an unexistent stream name 36 | Then returns a filtered sdp to offer to remote peer -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/e2e/View.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 |
24 |

View

25 |
26 | 27 |
28 |
29 |
30 | 33 |
34 |
35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
MediaTrackCodecFrame WidthFrame HeightFPSBytes received (total)Packet loss delta (sec.)Packet loss ratioPacket loss (total)Jitter (sec.)Bitrate (kbps)Timestamp
55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
Candidate typeCurrent RTT (sec.)Total RTT (sec.)
65 |
66 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/GetPublisherConnectionPath.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to publish to a Millicast Stream so I can get a connection path 2 | 3 | Scenario: Publish with an existing stream name and valid token 4 | Given I have a valid token and an existing stream name 5 | When I request a connection path to Director API 6 | Then I get the publish connection path 7 | 8 | Scenario: Publish with an unexisting stream name and valid token 9 | Given I have a valid token and an unexisting stream name 10 | When I request a connection path to Director API 11 | Then throws an error with "invalid stream name" message 12 | 13 | Scenario: Publish with an existing stream name and invalid token 14 | Given I have an invalid token and an existing stream name 15 | When I request a connection path to Director API 16 | Then throws an error with "invalid token" message 17 | 18 | Scenario: Publish with an existing stream name and valid token using other API Endpoint 19 | Given I have a valid token and an existing stream name 20 | When I request a connection path to Director API 21 | Then I get the publish connection path 22 | 23 | Scenario: Publish with an existing stream name, valid token and options as object 24 | Given I have a valid token and an existing stream name 25 | When I request a connection path to Director API using options object 26 | Then I get the publish connection path 27 | 28 | Scenario: Publish to an existing stream name, valid token and custom live websocket domain 29 | Given I have a valid token and an existing stream name 30 | When I set a custom live websocket domain and I request a connection path to Director API 31 | Then I get the publish connection path -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/View.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to subscribe to a stream without managing connections 2 | 3 | Scenario: Instance viewer without tokenGenerator 4 | Given no token generator 5 | When I instance a View 6 | Then throws an error 7 | 8 | Scenario: Subscribe to stream 9 | Given an instance of View 10 | When I subscribe to a stream with a connection path 11 | Then peer connection state is connected 12 | 13 | Scenario: Connect subscriber without connection path 14 | Given I want to subscribe 15 | When I instance a View with a token generator without connection path 16 | Then throws an error 17 | 18 | Scenario: Connect subscriber already connected 19 | Given an instance of View already connected 20 | When I connect again to the stream 21 | Then throws an error 22 | 23 | Scenario: Stop subscription 24 | Given I am subscribed to a stream 25 | When I stop the subscription 26 | Then peer connection and WebSocket are null 27 | 28 | Scenario: Stop inactive subscription 29 | Given I am not connected to a stream 30 | When I stop the subscription 31 | Then peer connection and WebSocket are null 32 | 33 | Scenario: Check status of active subscription 34 | Given I am subscribed to a stream 35 | When I check if subscription is active 36 | Then returns true 37 | 38 | Scenario: Check status of inactive subscription 39 | Given I am not subscribed to a stream 40 | When I check if subscription is active 41 | Then returns false 42 | 43 | Scenario: Subscribe to stream with invalid token generator 44 | Given an instance of View with invalid token generator 45 | When I subscribe to a stream 46 | Then throws token generator error -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/GetSubscriberConnectionPath.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to subscribe to a Millicast Stream so I can get a connection path 2 | 3 | Scenario: Subscribe to an existing unrestricted stream, valid accountId and no token 4 | Given I have an existing stream name, accountId and no token 5 | When I request a connection path to Director API 6 | Then I get the subscriber connection path 7 | 8 | Scenario: Subscribe to an existing restricted stream and valid token 9 | Given I have an existing stream name and valid token 10 | When I request a connection path to Director API 11 | Then I get the subscriber connection path 12 | 13 | Scenario: Subscribe to an existing unrestricted stream, invalid accountId and no token 14 | Given I have an existing stream name, invalid accountId and no token 15 | When I request a connection path to Director API 16 | Then throws an error with "stream not found" message 17 | 18 | Scenario: Subscribe to an existing stream using other API Endpoint 19 | Given I have an existing stream name, accountId and no token 20 | When I request a connection path to Director API 21 | Then I get the subscriber connection path 22 | 23 | Scenario: Subscribe to an existing unrestricted stream, valid accountId, no token and options as object 24 | Given I have an existing stream name, accountId and no token 25 | When I request a connection path to Director API using options object 26 | Then I get the subscriber connection path 27 | 28 | Scenario: Subscribe to an existing stream, valid accountId, no token and custom live websocket domain 29 | Given I have an existing stream name, accountId and no token 30 | When I set a custom live websocket domain and I request a connection path to Director API 31 | Then I get the subscriber connection path 32 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/SetRemoteDescription.steps.js: -------------------------------------------------------------------------------- 1 | import { loadFeature, defineFeature } from 'jest-cucumber' 2 | import PeerConnection from '../../src/PeerConnection' 3 | import './__mocks__/MockMediaStream' 4 | import './__mocks__/MockRTCPeerConnection' 5 | const feature = loadFeature('../features/SetRemoteDescription.feature', { loadRelativePath: true, errors: true }) 6 | 7 | defineFeature(feature, test => { 8 | afterEach(async () => { 9 | jest.restoreAllMocks() 10 | }) 11 | 12 | test('Setting remote SDP to RTC peer', ({ given, when, then }) => { 13 | const peerConnection = new PeerConnection() 14 | const sdp = 'My SDP' 15 | 16 | given('I got the peer', async () => { 17 | await peerConnection.createRTCPeer() 18 | }) 19 | 20 | when('I set the remote description', async () => { 21 | await peerConnection.setRTCRemoteSDP(sdp) 22 | }) 23 | 24 | then('the SDP is setted', async () => { 25 | expect(peerConnection.peer.currentRemoteDescription).toBeDefined() 26 | }) 27 | }) 28 | 29 | test('Error setting remote SDP to RTC peer', ({ given, when, then }) => { 30 | const peerConnection = new PeerConnection() 31 | const sdp = 'My SDP' 32 | let responseError 33 | 34 | given('I got the peer', async () => { 35 | await peerConnection.createRTCPeer() 36 | }) 37 | 38 | when('I set the remote description and peer returns an error', async () => { 39 | jest.spyOn(global.RTCPeerConnection.prototype, 'setRemoteDescription').mockRejectedValue(new Error('Invalid answer')) 40 | try { 41 | await peerConnection.setRTCRemoteSDP(sdp) 42 | } catch (error) { 43 | responseError = error 44 | } 45 | }) 46 | 47 | then('throws an error', async () => { 48 | expect(responseError.message).toBe('Invalid answer') 49 | }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/e2e/Puppeteer.steps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | import path from 'path' 6 | import puppeteer from 'puppeteer' 7 | import { loadFeature, defineFeature } from 'jest-cucumber' 8 | const feature = loadFeature('../features/Puppeteer.feature', { loadRelativePath: true, errors: true }) 9 | 10 | // Variables used for testing 11 | const pageLocation = `file:${path.join(__dirname, './PuppeteerJest.html')}` 12 | let browser = null 13 | let page = null 14 | 15 | defineFeature(feature, test => { 16 | afterEach(async () => { 17 | if (browser) { 18 | await browser.close() 19 | } 20 | browser = null 21 | page = null 22 | }) 23 | 24 | test('Load example page with Puppeteer', ({ given, when, then }) => { 25 | given('i have a browser opened', async () => { 26 | browser = await puppeteer.launch({ args: ['--no-sandbox'] }) 27 | }) 28 | 29 | when('i open a new page and go to the example web', async () => { 30 | page = await browser.newPage() 31 | await page.goto(pageLocation) 32 | }) 33 | 34 | then('the web page title says "PuppeteerJest"', async () => { 35 | await expect(page.title()).resolves.toMatch('PuppeteerJest') 36 | }) 37 | }, 100000) 38 | 39 | test('SDK loaded', ({ given, when, then }) => { 40 | let millicastModule = null 41 | 42 | given('i have a browser opened and an example page with the Millicast SDK', async () => { 43 | browser = await puppeteer.launch({ args: ['--no-sandbox'] }) 44 | page = await browser.newPage() 45 | await page.goto(pageLocation) 46 | }) 47 | 48 | when('i ask the "millicast" module', async () => { 49 | millicastModule = await page.evaluate('millicast') 50 | }) 51 | 52 | then('returns an instance of "millicast"', () => { 53 | expect(millicastModule).toBeDefined() 54 | }) 55 | }, 100000) 56 | }) 57 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasksRunnerOptions": { 3 | "default": { 4 | "runner": "nx/tasks-runners/default", 5 | "options": { 6 | "cacheableOperations": [ 7 | "build", 8 | "test", 9 | "lint", 10 | "lint:format", 11 | "prepare", 12 | "build-docs", 13 | "test-unit", 14 | "test-unit-coverage", 15 | "test-e2e", 16 | "test-all" 17 | ] 18 | } 19 | } 20 | }, 21 | "targetDefaults": { 22 | "test": { 23 | "dependsOn": [ 24 | "^test" 25 | ], 26 | "inputs": [ 27 | "{projectRoot}/**/*", 28 | "!{projectRoot}/**/*.md" 29 | ], 30 | "cache": true 31 | }, 32 | "prepare": { 33 | "dependsOn": [ 34 | "^prepare" 35 | ], 36 | "cache": true 37 | }, 38 | "preview": { 39 | "dependsOn": [ 40 | "^preview" 41 | ] 42 | }, 43 | "build:watch": { 44 | "dependsOn": [ 45 | "^build:watch" 46 | ] 47 | }, 48 | "build": { 49 | "outputs": [ 50 | "{projectRoot}//**/dist/*" 51 | ], 52 | "cache": true, 53 | "dependsOn": [] 54 | }, 55 | "start": { 56 | "dependsOn": [] 57 | }, 58 | "build-docs": { 59 | "cache": true, 60 | "dependsOn": [] 61 | }, 62 | "start-docs": { 63 | "dependsOn": [] 64 | }, 65 | "test-unit": { 66 | "cache": true, 67 | "dependsOn": [] 68 | }, 69 | "test-unit-coverage": { 70 | "cache": true, 71 | "dependsOn": [] 72 | }, 73 | "test-e2e": { 74 | "cache": true, 75 | "dependsOn": [] 76 | }, 77 | "test-all": { 78 | "cache": true, 79 | "dependsOn": [] 80 | } 81 | }, 82 | "affected": { 83 | "defaultBase": "main" 84 | } 85 | } -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/GetCapabilities.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to get browser audio/video capabilities so I can choose the codec to start broadcasting 2 | 3 | Scenario: Browser supports more codecs than Millicast 4 | Given my browser supports H264, H265, red and rtx 5 | When I get video capabilities 6 | Then returns H264 and H265 in codecs property 7 | 8 | Scenario: Browser supports all codecs of Millicast 9 | Given my browser supports H264, H265, VP8, VP9 and AV1 10 | When I get video capabilities 11 | Then returns all codecs 12 | 13 | Scenario: Browser supports SVC for VP9 14 | Given my browser supports VP9 with scalability modes 15 | When I get video capabilities 16 | Then returns VP9 with all scalability modes available 17 | 18 | Scenario: Browser supports SVC for VP9 repeated layers 19 | Given my browser supports VP9 with scalability modes repeated 20 | When I get video capabilities 21 | Then returns VP9 with all scalability modes available 22 | 23 | Scenario: Get video capabilities in Firefox 24 | Given I am in Firefeox 25 | When I get video capabilities 26 | Then returns H264, VP8 and VP9 codecs 27 | 28 | Scenario: Get audio capabilities in Chrome 29 | Given my browser audio capabilities 30 | When I get audio capabilities 31 | Then returns opus and multiopus codecs 32 | 33 | Scenario: Get audio capabilities in iOS Chrome 34 | Given my browser audio capabilities 35 | When I get audio capabilities 36 | Then returns opus codec 37 | 38 | Scenario: Get audio capabilities in Firefox 39 | Given my browser audio capabilities 40 | When I get audio capabilities 41 | Then returns opus codec 42 | 43 | Scenario: Get capabilities from inexistent kind in Chrome 44 | Given I am in Chrome 45 | When I get data capabilities 46 | Then returns null 47 | 48 | Scenario: Get capabilities from inexistent kind in Firefox 49 | Given I am in Firefox 50 | When I get data capabilities 51 | Then returns null -------------------------------------------------------------------------------- /packages/millicast-webaudio-delay-demo/README.md: -------------------------------------------------------------------------------- 1 | # Millicast WebAudio Delay Demo 2 | 3 | The WebAudio Delay demo application lets you interact with audio delay between broadcasting and viewing a Dolby.io real-time stream. By moving the red slider you can increase the latency and compare its impact on your audio. 4 | 5 | 6 | 7 | ## Getting started 8 | 9 | 1. Go to the [Dolby.io Streaming dashboard](https://dashboard.dolby.io/) and select your token. If you do not have a token, create it by clicking the **create** button. 10 | 11 | 2. Locate your `account ID` in the **token details** tab and copy the token. 12 | 13 | 3. Select the **publishing** tab and copy your `stream name`. 14 | 15 | 4. Open the Millicast SDK in a code editor, create a `.env` file in the `millicast-webaudio-delay-demo` folder, and add the following data to the file: 16 | 17 | ```sh 18 | MILLICAST_STREAM_NAME=yourStreamName 19 | MILLICAST_ACCOUNT_ID=yourAccountId 20 | ``` 21 | 22 | This content is also available in the `.env.sample` file. 23 | 24 | 5. Replace `yourStreamName` and `yourAccountId` with the data copied from the dashboard. 25 | 26 | 6. Open a terminal in the `millicast-webaudio-delay-demo` folder. 27 | 28 | 7. Install all dependencies: 29 | ```sh 30 | npm ci 31 | ``` 32 | 8. Run the application: 33 | ```sh 34 | npm start 35 | ``` 36 | 37 | 9. Open `http://localhost:10003` and test the application. 38 | 39 | You need to broadcast a stream to experience the latency. You can do it either via the Dolby.io dashboard by clicking the **broadcast** button, located next to your token, or you can use the [Publisher](../millicast-publisher-demo/) demo application. After you start broadcasting, you can move the red slider in the application to change the latency. To listen to the broadcasted audio, we recommend using the [Viewer](../millicast-viewer-demo/) demo application. 40 | 41 | ## Introducing updates 42 | After introducing any changes to the `public` directory, use the following command: 43 | ``` 44 | npm run prepare 45 | ``` -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/PeerStats.steps.js: -------------------------------------------------------------------------------- 1 | import PeerConnectionStats, { peerConnectionStatsEvents } from '../../src/PeerConnectionStats' 2 | 3 | jest.mock('events') 4 | 5 | jest.mock('../../src/workers/TransformWorker.worker.js', () => 6 | jest.fn(() => ({ 7 | postMessage: jest.fn(), 8 | terminate: jest.fn() 9 | })) 10 | ) 11 | 12 | describe('PeerConnectionStats', () => { 13 | let mockPeer, mockStatsInput, mockStatsOutput, statsInstance 14 | 15 | beforeEach(() => { 16 | mockPeer = {} 17 | mockStatsInput = { 18 | input: { 19 | audio: [], 20 | video: [] 21 | }, 22 | output: { 23 | audio: [], 24 | video: [] 25 | } 26 | } 27 | mockStatsOutput = { 28 | audio: { 29 | inbounds: [], 30 | outbounds: [] 31 | }, 32 | video: { 33 | inbounds: [], 34 | outbounds: [] 35 | } 36 | } 37 | statsInstance = new PeerConnectionStats(mockPeer) 38 | }) 39 | 40 | test('initializes stats collection when autoInitStats is true (default)', () => { 41 | expect(statsInstance.collection).not.toBeNull() 42 | }) 43 | 44 | test('does not initialize stats collection when autoInitStats is false', () => { 45 | statsInstance = new PeerConnectionStats(mockPeer, { autoInitStats: false }) 46 | expect(statsInstance.collection).toBeNull() 47 | }) 48 | 49 | test('stop - stops stats collection', () => { 50 | const mockCollection = { stop: jest.fn() } 51 | statsInstance.collection = mockCollection 52 | statsInstance.stop() 53 | expect(mockCollection.stop).toHaveBeenCalledTimes(1) 54 | }) 55 | 56 | test('emits stats event when stats are received', () => { 57 | const emitSpy = jest.spyOn(statsInstance, 'emit') 58 | 59 | // Simulate the collection receiving stats 60 | statsInstance.collection.emit('stats', mockStatsInput) 61 | 62 | expect(emitSpy).toHaveBeenCalledTimes(1) 63 | expect(emitSpy).toHaveBeenCalledWith( 64 | peerConnectionStatsEvents.stats, 65 | mockStatsOutput 66 | ) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /packages/millicast-publisher-demo/src/js/test/MillicastPublishUserMedia.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |

MillicastPublishUserMedia

6 |
7 |

READY!

8 | 14 |
15 |
16 | Bitrate: 17 | 28 | 33 | 34 | 39 | 40 |
41 |
42 | Microphone (audio input): 43 | 48 | Speakers (audio output): 49 | 50 | Video: 51 | 56 |
57 |
58 | 61 | 64 |
65 |
66 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/ViewerReconnection.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to reconnect to a stream so i can watch streams without manual reconnections 2 | 3 | Scenario: Reconnection when peer has an error 4 | Given an instance of Viewer with reconnection enabled 5 | When peer has an error 6 | Then reconnection is called 7 | 8 | Scenario: No reconnection when peer has not an error 9 | Given an instance of Viewer with reconnection enabled 10 | When peer change status to connected 11 | Then reconnection is not called 12 | 13 | Scenario: Reconnection when signaling has an error 14 | Given an instance of Viewer with reconnection enabled 15 | When signaling has an error 16 | Then reconnection is called 17 | 18 | Scenario: No reconnect when signaling has an error and reconnection is already being executed 19 | Given an instance of Viewer with reconnection enabled 20 | When reconnect was called and signaling has an error 21 | Then reconnection is not called 22 | 23 | Scenario: Reconnection disabled when peer has an error 24 | Given an instance of Viewer with reconnection disabled 25 | When peer has an error 26 | Then reconnection is not called 27 | 28 | Scenario: Reconnection when peer has a disconnection 29 | Given an instance of Viewer with reconnection enabled 30 | When peer has a disconnection 31 | Then waits and call reconnection 32 | 33 | Scenario: Reconnection interval when peer has an error 34 | Given an instance of Viewer with reconnection enabled and peer with error 35 | When reconnection is called and fails 36 | Then reconnection is called again in increments of 2 seconds until 32 seconds 37 | 38 | Scenario: Reconnection when peer has recover from error 39 | Given an instance of Viewer with reconnection enabled 40 | When reconnection is called and peer is currently connected 41 | Then reconnection is not called again 42 | 43 | Scenario: Reconnection and peer has recover from error 44 | Given an instance of Viewer with reconnection enabled 45 | When reconnection is called and peer is inactive 46 | Then peer reconnects and reconnection is not called again -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/PublisherReconnection.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to reconnect to my broadcast so i can stream without manual reconnections 2 | 3 | Scenario: Reconnection when peer has an error 4 | Given an instance of Publish with reconnection enabled 5 | When peer has an error 6 | Then reconnection is called 7 | 8 | Scenario: No reconnection when peer has not an error 9 | Given an instance of Publish with reconnection enabled 10 | When peer change status to connected 11 | Then reconnection is not called 12 | 13 | Scenario: Reconnection when signaling has an error 14 | Given an instance of Publish with reconnection enabled 15 | When signaling has an error 16 | Then reconnection is called 17 | 18 | Scenario: No reconnect when signaling has an error and reconnection is already being executed 19 | Given an instance of Publish with reconnection enabled 20 | When reconnect was called and signaling has an error 21 | Then reconnection is not called 22 | 23 | Scenario: Reconnection disabled when peer has an error 24 | Given an instance of Publish with reconnection disabled 25 | When peer has an error 26 | Then reconnection is not called 27 | 28 | Scenario: Reconnection when peer has a disconnection 29 | Given an instance of Publish with reconnection enabled 30 | When peer has a disconnection 31 | Then waits and call reconnection 32 | 33 | Scenario: Reconnection interval when peer has an error 34 | Given an instance of Publish with reconnection enabled and peer with error 35 | When reconnection is called and fails 36 | Then reconnection is called again in increments of 2 seconds until 32 seconds 37 | 38 | Scenario: Reconnection when peer has recover from error 39 | Given an instance of Publish with reconnection enabled 40 | When reconnection is called and peer is currently connected 41 | Then reconnection is not called again 42 | 43 | Scenario: Reconnection and peer has recover from error 44 | Given an instance of Publish with reconnection enabled 45 | When reconnection is called and peer is inactive 46 | Then peer reconnects and reconnection is not called again -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "husky": { 5 | "hooks": { 6 | "pre-commit": "lint-staged", 7 | "pre-push": "./check-signature.sh" 8 | } 9 | }, 10 | "lint-staged": { 11 | "**/*.js": [ 12 | "eslint --fix", 13 | "git add" 14 | ] 15 | }, 16 | "workspaces": [ 17 | "packages/*" 18 | ], 19 | "scripts": { 20 | "prepare": "lerna bootstrap --ci", 21 | "build": "lerna run build", 22 | "build:sdk": "lerna run build:watch --scope '@millicast/sdk' --parallel", 23 | "start:projects": "lerna run start --scope 'millicast-publisher-demo' --scope 'millicast-viewer-demo' --parallel", 24 | "delay": "node -e \"setTimeout(() => {}, 3000)\"", 25 | "start": "concurrently \"npm run build:sdk\" \"npm run delay && npm run start:projects\"", 26 | "start-all:projects": "lerna run start --parallel", 27 | "start-all": "concurrently \"npm run build:sdk\" \"npm run delay && npm run start-all:projects\"", 28 | "test": "lerna run test --stream", 29 | "test-e2e": "lerna run test-e2e --stream", 30 | "build-docs": "lerna run build-docs", 31 | "start-docs": "lerna run start-docs --stream", 32 | "publish": "lerna publish from-git", 33 | "lint-fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix" 34 | }, 35 | "devDependencies": { 36 | "@changesets/cli": "^2.27.1", 37 | "@lerna/publish": "^5.5.0", 38 | "babel-eslint": "^10.1.0", 39 | "concurrently": "^8.2.2", 40 | "eslint": "^8.2.0", 41 | "eslint-config-airbnb": "^19.0.4", 42 | "eslint-config-standard": "^17.1.0", 43 | "eslint-plugin-flowtype": "^8.0.3", 44 | "eslint-plugin-import": "^2.22.1", 45 | "eslint-plugin-jest": "^28.13.3", 46 | "eslint-plugin-jsx-a11y": "^6.4.1", 47 | "eslint-plugin-node": "^11.1.0", 48 | "eslint-plugin-promise": "^6.0.0", 49 | "eslint-plugin-standard": "^5.0.0", 50 | "husky": "^4.3.8", 51 | "install": "^0.13.0", 52 | "lerna": "^5.5.0", 53 | "lint-staged": "^10.5.4", 54 | "netlify-cli": "^22.1.3", 55 | "npm": "^11.4.2", 56 | "vite": "^6.3.5" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/new-release.yml: -------------------------------------------------------------------------------- 1 | name: New Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | build: 12 | if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref, 'rc') 13 | runs-on: ubuntu-latest 14 | 15 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 16 | permissions: 17 | contents: read 18 | pages: write # to deploy to Pages 19 | id-token: write # to verify the deployment originates from an appropriate source 20 | 21 | # Deploy to the github-pages environment 22 | environment: 23 | name: github-pages 24 | url: ${{ steps.deployment.outputs.page_url }} 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - name: Install the dependencies 🧱 30 | run: npm ci 31 | 32 | - name: Build All 🔧 33 | run: | 34 | npm run build 35 | npm run build-docs 36 | 37 | - name: Test 38 | run: npm test 39 | env: 40 | ACCOUNT_ID: ${{vars.PUBLISHER_DEMO_ACC_ID}} 41 | PUBLISH_TOKEN: ${{secrets.PUBLISHER_DEMO_TOKEN}} 42 | 43 | - name: Setup GitHub Pages 🛠 44 | uses: actions/configure-pages@v5 45 | 46 | - name: Upload artifact ⬆️ 47 | uses: actions/upload-pages-artifact@v3 48 | with: 49 | path: packages/millicast-sdk/docs 50 | 51 | - name: Deploy to GitHub Pages 🚀 52 | id: deployment 53 | uses: actions/deploy-pages@v4 54 | 55 | publish-npm: 56 | if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref, 'rc') 57 | needs: build 58 | runs-on: ubuntu-latest 59 | steps: 60 | - uses: actions/checkout@v4 61 | 62 | - name: Install the dependencies 🧱 63 | run: npm ci 64 | 65 | - name: Add Readme to package 66 | run: cp README.md packages/millicast-sdk/README.md 67 | - name: Login to npm 68 | run: | 69 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc 70 | - name: Publish package 71 | working-directory: './packages/millicast-sdk' 72 | run: | 73 | npm run build 74 | npm publish --access public 75 | env: 76 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 77 | -------------------------------------------------------------------------------- /packages/millicast-chromecast-receiver/src/viewer.js: -------------------------------------------------------------------------------- 1 | import { View, Director, Logger } from '@millicast/sdk' 2 | 3 | window.Logger = Logger 4 | 5 | if (import.meta.env.MILLICAST_DIRECTOR_ENDPOINT) { 6 | Director.setEndpoint(import.meta.env.MILLICAST_DIRECTOR_ENDPOINT) 7 | } 8 | 9 | const addStream = (stream) => { 10 | const video = document.querySelector('#player') 11 | // Create new video element 12 | video.srcObject = stream 13 | } 14 | 15 | const removeStream = () => { 16 | const video = document.querySelector('#player') 17 | // Create new video element 18 | video.srcObject = null 19 | } 20 | 21 | const subscribe = async (streamName, streamAccountId) => { 22 | const tokenGenerator = () => Director.getSubscriber(streamName, streamAccountId) 23 | const millicastView = new View(streamName, tokenGenerator) 24 | millicastView.on('broadcastEvent', (event) => { 25 | const layers = event.data.layers !== null ? event.data.layers : {} 26 | if (event.name === 'layers' && Object.keys(layers).length <= 0) { 27 | // call play logic or being reconnect interval 28 | close().then(() => { 29 | subscribe(streamName, streamAccountId) 30 | }) 31 | console.error('Feed no longer found.') 32 | } 33 | }) 34 | 35 | millicastView.on('newTrack', (event) => { 36 | addStream(event.streams[0]) 37 | }) 38 | 39 | const close = () => { 40 | removeStream() 41 | millicastView.millicastSignaling?.close() 42 | return Promise.resolve({}) 43 | } 44 | 45 | try { 46 | await millicastView.connect() 47 | } catch (error) { 48 | close().then(() => { 49 | subscribe(streamName, streamAccountId) 50 | }) 51 | console.error(error) 52 | } 53 | } 54 | 55 | const context = cast.framework.CastReceiverContext.getInstance() 56 | const player = context.getPlayerManager() 57 | 58 | player.setMediaElement(document.querySelector('#player')) 59 | 60 | /** 61 | * Intercept the LOAD request to be able to read in a contentId and get data. 62 | */ 63 | player.setMessageInterceptor( 64 | cast.framework.messages.MessageType.LOAD, loadRequestData => { 65 | const media = loadRequestData.media 66 | const { streamName, streamAccountId } = media.customData 67 | 68 | subscribe(streamName, streamAccountId) 69 | 70 | loadRequestData.media.contentUrl = 'https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4' 71 | loadRequestData.media.contentType = 'video/mp4' 72 | 73 | return loadRequestData 74 | } 75 | ) 76 | 77 | context.start() 78 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Changesets is a tool to keep record of the changes for a future release. This way we can improve the generation of Release Notes by creating small notes per feature and then grouping them together. 4 | 5 | ## Changes per feature 6 | Each time a new change is done for a release, a changesets should be created with a summary of the change and a version type (`patch`, `minor`, `major`) depending on what was agreed previously. 7 | 8 | To do so, developers should run the following command: 9 | 10 | ```bash 11 | npx changeset 12 | ``` 13 | It will be displayed the following options, were it will set the version type of the change. 14 | 15 | ``` 16 | 🦋 What kind of change is this for @millicast/sdk? (current version is X.X.X) … 17 | ❯ patch 18 | minor 19 | major 20 | ``` 21 | 22 | After selecting the kind of change, it will prompt an input to write the description of the change: 23 | 24 | ``` 25 | 🦋 What kind of change is this for @millicast/sdk? (current version is X.X.X) · patch 26 | 🦋 Please enter a summary for this change (this will be in the changelogs). 27 | 🦋 (submit empty line to open external editor) 28 | 🦋 Summary › This is a summary test 29 | ``` 30 | 31 | Then, a confirmation of the changeset will wait until everything looks good (press `Enter` key or type `Y` and `Enter`): 32 | 33 | ``` 34 | 🦋 What kind of change is this for @millicast/sdk? (current version is X.X.X) · patch 35 | 🦋 Please enter a summary for this change (this will be in the changelogs). 36 | 🦋 (submit empty line to open external editor) 37 | 🦋 Summary · This is a summary test 38 | 🦋 39 | 🦋 === Summary of changesets === 40 | 🦋 patch: @millicast/sdk 41 | 🦋 42 | 🦋 Is this your desired changeset? (Y/n) › true 43 | ``` 44 | 45 | A new Markdown file will be created in the `./.changesets` folder with a unique name. This then will be used when trying to generate the final changelog. 46 | 47 | ## Creating CHANGELOG.md 48 | When the code is ready to be released, and the Release Notes to be created, the following command creates the `CHANGELOG.md` file: 49 | 50 | ``` 51 | npx changeset version 52 | ``` 53 | 54 | This will take and remove all the changesets added previously and join them into a `CHANGELOG.md` file in `./packages/millicast-sdk` directory. It will also bump the version number from `./packages/millicast-sdk/package.json`. The terminal will display a success message like this one: 55 | 56 | ``` 57 | 🦋 All files have been updated. Review them and commit at your leisure 58 | ``` -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/e2e/PuppeteerJest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | PuppeteerJest 4 | 5 | 52 | 53 | 54 |

PuppeteerJest

55 |
56 |

Run 'npm test' for test with Jest!

57 |
58 | 59 | 60 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/Publish.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to publish a stream without managing connections 2 | 3 | Scenario: Instance publisher without tokenGenerator 4 | Given no token generator 5 | When I instance a Publish 6 | Then throws an error 7 | 8 | Scenario: Broadcast stream 9 | Given an instance of Publish with connection path 10 | When I broadcast a stream with media stream 11 | Then peer connection state is connected 12 | 13 | Scenario: Broadcast stream default options 14 | Given an instance of Publish 15 | When I broadcast a stream without options 16 | Then throws an error 17 | 18 | Scenario: Broadcast with invalid codec 19 | Given an instance of Publish 20 | When I broadcast with unsupported codec 21 | Then throws an error 22 | 23 | Scenario: Broadcast with non-default codec 24 | Given an instance of Publish 25 | When I broadcast a stream with H265 codec 26 | Then peer connection state is connected 27 | 28 | Scenario: Broadcast without connection path 29 | Given I want to broadcast 30 | When I instance a Publish with token generator without connection path 31 | Then throws an error 32 | 33 | Scenario: Broadcast without mediaStream 34 | Given an instance of Publish 35 | When I broadcast a stream without a mediaStream 36 | Then throws an error 37 | 38 | Scenario: Broadcast to active publisher 39 | Given an instance of Publish already connected 40 | When I broadcast again to the stream 41 | Then throws an error 42 | 43 | Scenario: Broadcast stream with bandwidth restriction 44 | Given an instance of Publish 45 | When I broadcast a stream with bandwidth restriction 46 | Then peer connection state is connected 47 | 48 | Scenario: Stop publish 49 | Given I am publishing a stream 50 | When I stop the publish 51 | Then peer connection and WebSocket are null 52 | 53 | Scenario: Stop inactive publish 54 | Given I am not publishing a stream 55 | When I stop the publish 56 | Then peer connection and WebSocket are null 57 | 58 | Scenario: Check status of active publish 59 | Given I am publishing a stream 60 | When I check if publish is active 61 | Then returns true 62 | 63 | Scenario: Check status of inactive publish 64 | Given I am not publishing a stream 65 | When I check if publish is active 66 | Then returns false 67 | 68 | Scenario: Broadcast to stream with invalid token generator 69 | Given an instance of Publish with invalid token generator 70 | When I broadcast a stream 71 | Then throws token generator error 72 | 73 | Scenario: Broadcast to stream with record option but no record available from token 74 | Given an instance of Publish with valid token generator with no recording available 75 | When I broadcast a stream 76 | Then throws an error -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/GetPeerStatus.steps.js: -------------------------------------------------------------------------------- 1 | import { loadFeature, defineFeature } from 'jest-cucumber' 2 | import PeerConnection from '../../src/PeerConnection' 3 | import './__mocks__/MockMediaStream' 4 | import './__mocks__/MockRTCPeerConnection' 5 | import MockRTCPeerConnectionNoConnectionState from './__mocks__/MockRTCPeerConnectionNoConnectionState' 6 | const feature = loadFeature('../features/GetPeerStatus.feature', { loadRelativePath: true, errors: true }) 7 | 8 | defineFeature(feature, test => { 9 | afterEach(async () => { 10 | jest.restoreAllMocks() 11 | }) 12 | 13 | test('Get existing RTC peer status', ({ given, when, then }) => { 14 | const peerConnection = new PeerConnection() 15 | let status 16 | 17 | given('I have a peer instanced', async () => { 18 | await peerConnection.createRTCPeer() 19 | }) 20 | 21 | when('I want to get the peer connection state', () => { 22 | status = peerConnection.getRTCPeerStatus() 23 | }) 24 | 25 | then('returns the connection state', async () => { 26 | expect(status).toBe('new') 27 | }) 28 | }) 29 | 30 | test('Get unexisting RTC peer status', ({ given, when, then }) => { 31 | const peerConnection = new PeerConnection() 32 | let status 33 | 34 | given('I do not have a peer connected', async () => {}) 35 | 36 | when('I want to get the peer connection state', () => { 37 | status = peerConnection.getRTCPeerStatus() 38 | }) 39 | 40 | then('returns no value', async () => { 41 | expect(status).toBeNull() 42 | }) 43 | }) 44 | 45 | test('Get connecting RTC peer status without connectionState', ({ given, when, then }) => { 46 | const peerConnection = new PeerConnection() 47 | let status 48 | 49 | given('I have a peer connecting without connectionState', async () => { 50 | global.RTCPeerConnection = MockRTCPeerConnectionNoConnectionState 51 | await peerConnection.createRTCPeer() 52 | peerConnection.peer.iceConnectionState = 'checking' 53 | }) 54 | 55 | when('I want to get the peer connection state', () => { 56 | status = peerConnection.getRTCPeerStatus() 57 | }) 58 | 59 | then('returns the connection state', async () => { 60 | expect(status).toBe('connecting') 61 | }) 62 | }) 63 | 64 | test('Get connected RTC peer status without connectionState', ({ given, when, then }) => { 65 | const peerConnection = new PeerConnection() 66 | let status 67 | 68 | given('I have a peer connected without connectionState', async () => { 69 | global.RTCPeerConnection = MockRTCPeerConnectionNoConnectionState 70 | await peerConnection.createRTCPeer() 71 | peerConnection.peer.iceConnectionState = 'completed' 72 | }) 73 | 74 | when('I want to get the peer connection state', () => { 75 | status = peerConnection.getRTCPeerStatus() 76 | }) 77 | 78 | then('returns the connection state', async () => { 79 | expect(status).toBe('connected') 80 | }) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/e2e/FunctionalPublish.steps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | import path from 'path' 6 | import puppeteer from 'puppeteer' 7 | import { loadFeature, defineFeature } from 'jest-cucumber' 8 | const feature = loadFeature('../features/FunctionalPublish.feature', { loadRelativePath: true, errors: true }) 9 | 10 | jest.setTimeout(20000) 11 | const pageLocation = `file:${path.join(__dirname, './PuppeteerJest.html')}` 12 | const publishToken = process.env.PUBLISH_TOKEN 13 | const streamName = process.env.STREAM_NAME ?? 'demo_' + Math.round(Math.random() * 100) + '_' + new Date().getTime() 14 | const accountId = process.env.ACCOUNT_ID 15 | const startPublisher = () => null 16 | const startViewer = () => null 17 | const defaultOptions = { 18 | bandwidth: 0, 19 | disableVideo: false, 20 | disableAudio: false, 21 | simulcast: false, 22 | scalabilityMode: null 23 | } 24 | let browser = null 25 | const sleep = ms => new Promise(function (resolve) { setTimeout(resolve, ms) }) 26 | 27 | afterEach(async () => { 28 | if (browser) { 29 | await browser.close() 30 | } 31 | browser = null 32 | }) 33 | 34 | beforeEach(async () => { 35 | browser = await puppeteer.launch({ 36 | // executablePath: process.env.CHROME_LOCATION, 37 | args: [ 38 | '--no-sandbox', 39 | '--use-fake-device-for-media-stream', 40 | '--use-fake-ui-for-media-stream' 41 | ] 42 | }) 43 | }) 44 | 45 | defineFeature(feature, test => { 46 | test('Broadcasting stream', ({ given, when, then }) => { 47 | let broadcastPage 48 | let viewerPage 49 | let isActive 50 | let videoFrame1 51 | let videoFrame2 52 | let options 53 | 54 | given(/^a page with view options and a page with broadcaster options and codec (.*)$/, async (codec) => { 55 | broadcastPage = await browser.newPage() 56 | viewerPage = await browser.newPage() 57 | await broadcastPage.goto(pageLocation) 58 | await viewerPage.goto(pageLocation) 59 | options = { 60 | ...defaultOptions, 61 | codec 62 | } 63 | }) 64 | 65 | when('I broadcast a stream and connect to stream as viewer', async () => { 66 | await broadcastPage.evaluate(({ options, publishToken, streamName }) => startPublisher(publishToken, streamName, options), { options, publishToken, streamName }) 67 | await viewerPage.evaluate(({ streamName, accountId }) => startViewer(streamName, accountId), { streamName, accountId }) 68 | 69 | isActive = await broadcastPage.evaluate('window.publish.isActive()') 70 | 71 | videoFrame1 = await viewerPage.evaluate('getVideoPixelSums()') 72 | await sleep(500) 73 | videoFrame2 = await viewerPage.evaluate('getVideoPixelSums()') 74 | }) 75 | 76 | then('broadcast is active and Viewer receive video data', async () => { 77 | expect(isActive).toBeTruthy() 78 | expect(videoFrame1).not.toBe(0) 79 | expect(videoFrame2).not.toBe(0) 80 | expect(videoFrame1).not.toEqual(videoFrame2) 81 | }) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/SetLocalDescription.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to set my local session description so I can broadcast or view a stream 2 | 3 | Scenario: Get RTC Local SDP as subscriber role 4 | Given I do not have options 5 | When I want to get the RTC Local SDP 6 | Then returns the SDP 7 | 8 | Scenario: Get RTC Local SDP without video as subscriber role 9 | Given I want local SDP without video 10 | When I want to get the RTC Local SDP 11 | Then returns the SDP 12 | 13 | Scenario: Get RTC Local SDP without audio as subscriber role 14 | Given I want local SDP without audio 15 | When I want to get the RTC Local SDP 16 | Then returns the SDP 17 | 18 | Scenario: Get RTC Local SDP as publisher role with valid MediaStream 19 | Given I have a MediaStream with 1 audio track and 1 video track and I want support stereo 20 | When I want to get the RTC Local SDP 21 | Then returns the SDP 22 | 23 | Scenario: Get RTC Local SDP as publisher role without video 24 | Given I have a MediaStream with 1 audio track and 1 video track 25 | When I want to get the RTC Local SDP without video 26 | Then returns the SDP 27 | 28 | Scenario: Get RTC Local SDP as publisher role without audio 29 | Given I have a MediaStream with 1 audio track and 1 video track 30 | When I want to get the RTC Local SDP without audio 31 | Then returns the SDP 32 | 33 | Scenario: Get RTC Local SDP as publisher role with simulcast and valid MediaStream 34 | Given I have a MediaStream with 1 audio track and 1 video track and I want support simulcast 35 | When I want to get the RTC Local SDP 36 | Then returns the SDP 37 | 38 | Scenario: Get RTC Local SDP as publisher role with invalid MediaStream 39 | Given I have a MediaStream with 2 video tracks and no audio track 40 | When I want to get the RTC Local SDP 41 | Then throw invalid MediaStream error 42 | 43 | Scenario: Get RTC Local SDP as publisher role with valid list of tracks 44 | Given I have a list of tracks with 1 audio track and 1 video track 45 | When I want to get the RTC Local SDP 46 | Then returns the SDP 47 | 48 | Scenario: Get RTC Local SDP as publisher role with invalid list of tracks 49 | Given I have a list of tracks with 3 audio tracks and 1 video track 50 | When I want to get the RTC Local SDP 51 | Then throw invalid MediaStream error 52 | 53 | Scenario: Get RTC Local SDP with scalability mode, valid MediaStream and using Chrome 54 | Given I am using Chrome and I have a MediaStream with 1 audio track and 1 video track and I want to support L1T3 mode 55 | When I want to get the RTC Local SDP 56 | Then returns the SDP with scalability mode 57 | 58 | Scenario: Get RTC Local SDP with scalability mode, valid MediaStream and using Firefox 59 | Given I am using Firefox and I have a MediaStream with 1 audio track and 1 video track and I want to support L1T3 mode 60 | When I want to get the RTC Local SDP 61 | Then returns the SDP without scalability mode -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/ManagePeerConnection.steps.js: -------------------------------------------------------------------------------- 1 | import { loadFeature, defineFeature } from 'jest-cucumber' 2 | import PeerConnection, { webRTCEvents } from '../../src/PeerConnection' 3 | import { defaultConfig } from './__mocks__/MockRTCPeerConnection' 4 | import './__mocks__/MockMediaStream' 5 | const feature = loadFeature('../features/ManagePeerConnection.feature', { loadRelativePath: true, errors: true }) 6 | 7 | defineFeature(feature, test => { 8 | afterEach(async () => { 9 | jest.restoreAllMocks() 10 | }) 11 | 12 | test('Get RTC peer without configuration', ({ given, when, then }) => { 13 | let peerConnection = null 14 | let peer = null 15 | 16 | given('I have no configuration', async () => { 17 | peerConnection = new PeerConnection() 18 | }) 19 | 20 | when('I get the RTC peer', async () => { 21 | await peerConnection.createRTCPeer() 22 | peer = peerConnection.getRTCPeer() 23 | }) 24 | 25 | then('returns the peer', async () => { 26 | expect(peer.getConfiguration()).toMatchObject({ ...defaultConfig, bundlePolicy: 'balanced' }) 27 | }) 28 | }) 29 | 30 | test('Get RTC peer without instance previously', ({ given, when, then }) => { 31 | let peerConnection = null 32 | let peer = null 33 | 34 | given('I have no configuration', async () => { 35 | peerConnection = new PeerConnection() 36 | }) 37 | 38 | when('I get the RTC peer without instance first', async () => { 39 | peer = peerConnection.getRTCPeer() 40 | }) 41 | 42 | then('returns null', async () => { 43 | expect(peer).toBeNull() 44 | }) 45 | }) 46 | 47 | test('Get RTC peer with configuration', ({ given, when, then }) => { 48 | let peerConnection = null 49 | let peer = null 50 | 51 | given('I have configuration', async () => { 52 | peerConnection = new PeerConnection() 53 | }) 54 | 55 | when('I get the RTC peer', async () => { 56 | await peerConnection.createRTCPeer({ 57 | bundlePolicy: 'max-bundle' 58 | }) 59 | peer = peerConnection.getRTCPeer() 60 | }) 61 | 62 | then('returns the peer', async () => { 63 | expect(peer).toMatchObject(peerConnection.peer) 64 | expect(peer.getConfiguration()).toMatchObject({ 65 | bundlePolicy: 'max-bundle' 66 | }) 67 | }) 68 | }) 69 | 70 | test('Close existing RTC peer', ({ given, when, then }) => { 71 | const handler = jest.fn() 72 | let peerConnection = null 73 | 74 | given('I have a RTC peer', async () => { 75 | peerConnection = new PeerConnection() 76 | await peerConnection.createRTCPeer() 77 | peerConnection.on(webRTCEvents.connectionStateChange, handler) 78 | }) 79 | 80 | when('I close the RTC peer', async () => { 81 | await peerConnection.closeRTCPeer() 82 | }) 83 | 84 | then('the peer is closed and emits connectionStateChange event', async () => { 85 | expect(handler).toHaveBeenCalledTimes(1) 86 | expect(handler).toHaveBeenCalledWith('closed') 87 | expect(peerConnection.peer).toBeNull() 88 | }) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/ChangeMediaTrack.steps.js: -------------------------------------------------------------------------------- 1 | import { loadFeature, defineFeature } from 'jest-cucumber' 2 | import PeerConnection from '../../src/PeerConnection' 3 | import './__mocks__/MockMediaStream' 4 | import './__mocks__/MockRTCPeerConnection' 5 | import './__mocks__/MockBrowser' 6 | const feature = loadFeature('../features/ChangeMediaTrack.feature', { loadRelativePath: true, errors: true }) 7 | 8 | defineFeature(feature, test => { 9 | afterEach(async () => { 10 | jest.restoreAllMocks() 11 | }) 12 | 13 | test('Replace track to existing peer', ({ given, when, then }) => { 14 | const peerConnection = new PeerConnection() 15 | const track = { id: 3, kind: 'audio', label: 'Audio2' } 16 | 17 | given('I have a peer connected', async () => { 18 | await peerConnection.createRTCPeer() 19 | const tracks = [{ id: 1, kind: 'audio', label: 'Audio1' }, { id: 2, kind: 'video', label: 'Video1' }] 20 | const mediaStream = new MediaStream(tracks) 21 | await peerConnection.getRTCLocalSDP({ mediaStream, disableVideo: false, disableAudio: false }) 22 | }) 23 | 24 | when('I want to change current audio track', () => { 25 | peerConnection.replaceTrack(track) 26 | }) 27 | 28 | then('the track is changed', async () => { 29 | expect(peerConnection.peer.getSenders()).toBeDefined() 30 | expect(peerConnection.peer.getSenders()).toEqual( 31 | expect.arrayContaining([ 32 | expect.objectContaining({ 33 | track: { ...track } 34 | }) 35 | ]) 36 | ) 37 | }) 38 | }) 39 | 40 | test('Replace track to unexisting peer', ({ given, when, then }) => { 41 | const peerConnection = new PeerConnection() 42 | const track = { id: 3, kind: 'audio', label: 'Audio2' } 43 | 44 | given('I do not have a peer connected', async () => {}) 45 | 46 | when('I want to change the audio track', () => { 47 | peerConnection.replaceTrack(track) 48 | }) 49 | 50 | then('the track is not changed', async () => { 51 | expect(peerConnection.peer).toBeNull() 52 | }) 53 | }) 54 | 55 | test('Replace unexisting track to peer', ({ given, when, then }) => { 56 | const peerConnection = new PeerConnection() 57 | const track = { id: 2, kind: 'audio', label: 'Audio2' } 58 | 59 | given('I have a peer connected with video track', async () => { 60 | await peerConnection.createRTCPeer() 61 | const tracks = [{ id: 1, kind: 'video', label: 'Video1' }] 62 | const mediaStream = new MediaStream(tracks) 63 | await peerConnection.getRTCLocalSDP({ mediaStream }) 64 | }) 65 | 66 | when('I want to change the audio track', () => { 67 | peerConnection.replaceTrack(track) 68 | }) 69 | 70 | then('the track is not changed', async () => { 71 | expect(peerConnection.peer.getSenders()).toBeDefined() 72 | expect(peerConnection.peer.getSenders()).toEqual( 73 | expect.not.arrayContaining([ 74 | expect.objectContaining({ 75 | track: { ...track } 76 | }) 77 | ]) 78 | ) 79 | }) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: 'CodeQL' 13 | 14 | on: 15 | push: 16 | branches: ['main', 'develop'] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ['main'] 20 | #schedule: 21 | # - cron: '34 2 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ['javascript'] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | - name: Autobuild 58 | uses: github/codeql-action/autobuild@v3 59 | 60 | # ℹ️ Command-line programs to run using the OS shell. 61 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 62 | 63 | # If the Autobuild fails above, remove it and uncomment the following three lines. 64 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 65 | 66 | # - run: | 67 | # echo "Run, Build Application using script" 68 | # ./location_of_script_within_repo/buildscript.sh 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v3 72 | with: 73 | category: '/language:${{matrix.language}}' -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/LoggerLevels.steps.js: -------------------------------------------------------------------------------- 1 | import { loadFeature, defineFeature } from 'jest-cucumber' 2 | 3 | let Logger 4 | beforeEach(() => { 5 | jest.isolateModules(() => { 6 | Logger = require('../../src/Logger').default 7 | }) 8 | }) 9 | 10 | const feature = loadFeature('../features/LoggerLevels.feature', { loadRelativePath: true, errors: true }) 11 | 12 | defineFeature(feature, test => { 13 | test('Set global level to INFO', ({ given, when, then }) => { 14 | given('global level is OFF', async () => { 15 | Logger.setLevel(Logger.OFF) 16 | }) 17 | 18 | when('I set global level to INFO', async () => { 19 | Logger.setLevel(Logger.INFO) 20 | }) 21 | 22 | then('new level is INFO', async () => { 23 | expect(Logger.getLevel()).toEqual(Logger.INFO) 24 | }) 25 | }) 26 | 27 | test('Set global level to INFO with named logger', ({ given, when, then }) => { 28 | let namedLogger 29 | given('global level is OFF and I have a named logger', async () => { 30 | Logger.setLevel(Logger.OFF) 31 | namedLogger = Logger.get('namedLogger') 32 | }) 33 | 34 | when('I set global level to INFO', async () => { 35 | Logger.setLevel(Logger.INFO) 36 | }) 37 | 38 | then('global and named logger level are at INFO', async () => { 39 | expect(Logger.getLevel()).toEqual(Logger.INFO) 40 | expect(namedLogger.getLevel()).toEqual(Logger.INFO) 41 | }) 42 | }) 43 | 44 | test('Set level of named logger', ({ given, when, then }) => { 45 | let namedLogger 46 | given('global level is OFF and I have a named logger', async () => { 47 | Logger.setLevel(Logger.OFF) 48 | namedLogger = Logger.get('namedLogger') 49 | }) 50 | 51 | when('I set named logger level to INFO', async () => { 52 | namedLogger.setLevel(Logger.INFO) 53 | }) 54 | 55 | then('global level is OFF and named logger level is INFO', async () => { 56 | expect(Logger.getLevel()).toEqual(Logger.OFF) 57 | expect(namedLogger.getLevel()).toEqual(Logger.INFO) 58 | }) 59 | }) 60 | 61 | test('Get named logger already created', ({ given, when, then }) => { 62 | let namedLogger 63 | let sameNamedLogger 64 | 65 | given('I have a named logger', async () => { 66 | namedLogger = Logger.get('namedLogger') 67 | }) 68 | 69 | when('I get a named logger with same name', async () => { 70 | sameNamedLogger = Logger.get('namedLogger') 71 | }) 72 | 73 | then('returns the same named logger', async () => { 74 | expect(namedLogger).toEqual(sameNamedLogger) 75 | }) 76 | }) 77 | 78 | test('Log message at logger level', ({ given, when, then }) => { 79 | const console = jest.spyOn(global.console, 'info') 80 | 81 | given('global level is INFO', async () => { 82 | Logger.setLevel(Logger.INFO) 83 | }) 84 | 85 | when('I log a message at INFO', async () => { 86 | Logger.info('This is a log message') 87 | }) 88 | 89 | then('a message is logged in console', async () => { 90 | expect(console).toHaveBeenCalledTimes(1) 91 | expect(console).toHaveBeenCalledWith(expect.any(String), 'This is a log message') 92 | }) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /packages/millicast-publisher-demo/src/js/test/MillicastMediaTest.js: -------------------------------------------------------------------------------- 1 | class MillicastMediaTest { 2 | constructor(constraints = undefined) { 3 | const defaultConstraints = { 4 | audio: true, 5 | video: true 6 | } 7 | const constraintsToUse = constraints ? constraints : defaultConstraints 8 | this.millicastMedia = new publisher.MillicastMedia(constraintsToUse) 9 | } 10 | 11 | async testGetMedia() { 12 | const mediaStream = await this.millicastMedia.getMedia() 13 | console.log('GetMedia response:', mediaStream) 14 | document.getElementById('millicast-media-video-test').srcObject = mediaStream 15 | return mediaStream 16 | } 17 | 18 | async testGetDevices() { 19 | const audioInputSelect = document.getElementById('audio-input-select') 20 | const audioOutputSelect = document.getElementById('audio-output-select') 21 | const videoSelect = document.getElementById('video-select') 22 | const devices = await this.millicastMedia.getMediaDevices() 23 | console.log('GetDevices response:', devices) 24 | this.fillSelectElement(audioInputSelect, devices.audioinput) 25 | this.fillSelectElement(audioOutputSelect, devices.audiooutput) 26 | this.fillSelectElement(videoSelect, devices.videoinput) 27 | return devices 28 | } 29 | 30 | fillSelectElement(selectElement, devices) { 31 | selectElement.innerHTML = '' 32 | for (const device of devices) 33 | selectElement.add(new Option(device.label, device.deviceId)) 34 | } 35 | 36 | testGetTracks() { 37 | const videoTrack = this.millicastMedia.videoInput 38 | const audioTrack = this.millicastMedia.audioInput 39 | console.log('Video track:', videoTrack) 40 | console.log('Audio track:', audioTrack) 41 | return { 42 | videoTrack, 43 | audioTrack, 44 | } 45 | } 46 | 47 | async testChangeAudio(selectObject) { 48 | const deviceId = selectObject.value 49 | const mediaStream = await this.millicastMedia.changeAudio(deviceId) 50 | console.log('ChangeAudio response:', mediaStream) 51 | document.getElementById('millicast-media-video-test').srcObject = mediaStream 52 | return mediaStream 53 | } 54 | 55 | async testChangeVideo(selectObject) { 56 | const deviceId = selectObject.value 57 | const mediaStream = await this.millicastMedia.changeVideo(deviceId) 58 | console.log('ChangeVideo response:', mediaStream) 59 | document.getElementById('millicast-media-video-test').srcObject = mediaStream 60 | return mediaStream 61 | } 62 | 63 | testMuteAudio() { 64 | const muted = this.millicastMedia.muteAudio(true) 65 | console.log('MuteAudio response:', muted) 66 | return muted 67 | } 68 | 69 | testMuteVideo() { 70 | const muted = this.millicastMedia.muteVideo(true) 71 | console.log('MuteVideo response:', muted) 72 | return muted 73 | } 74 | 75 | testUnmuteAudio() { 76 | const muted = this.millicastMedia.muteAudio(false) 77 | console.log('UnmuteAudio response:', muted) 78 | return muted 79 | } 80 | 81 | testUnmuteVideo() { 82 | const muted = this.millicastMedia.muteVideo(false) 83 | console.log('UnmuteVideo response:', muted) 84 | return muted 85 | } 86 | } 87 | 88 | const millicastMediaTest = new MillicastMediaTest() 89 | -------------------------------------------------------------------------------- /packages/millicast-webaudio-delay-demo/src/viewer.js: -------------------------------------------------------------------------------- 1 | import { View, Director, Logger } from '@millicast/sdk' 2 | import CircularSlider from '@maslick/radiaslider/src/slider-circular' 3 | console.log(CircularSlider) 4 | 5 | window.Logger = Logger 6 | 7 | Logger.setLevel(Logger.DEBUG) 8 | 9 | if (import.meta.env.MILLICAST_DIRECTOR_ENDPOINT) { 10 | Director.setEndpoint(import.meta.env.MILLICAST_DIRECTOR_ENDPOINT) 11 | } 12 | 13 | // Get our url 14 | const href = new URL(window.location.href) 15 | // Get or set Defaults 16 | const streamName = href.searchParams.get('streamName') 17 | ? href.searchParams.get('streamName') 18 | : import.meta.env.MILLICAST_STREAM_NAME 19 | const streamAccountId = href.searchParams.get('streamAccountId') 20 | ? href.searchParams.get('streamAccountId') 21 | : import.meta.env.MILLICAST_ACCOUNT_ID 22 | 23 | // MillicastView object 24 | let millicastView = null 25 | 26 | let delayNode 27 | const MaxDelay = 30 28 | 29 | document.body.onclick = async () => { 30 | document.body.onclick = () => {} 31 | 32 | document.getElementById('slider').removeChild(document.getElementById('play')) 33 | document.getElementById('myCanvas').style.display = 'inherit' 34 | 35 | const slider = new CircularSlider({ canvasId: 'myCanvas', continuousMode: true, x0: 150, y0: 150, readOnly: false }) 36 | slider.addSlider({ 37 | id: 1, 38 | radius: 80, 39 | min: 0, 40 | max: 30, 41 | step: 5, 42 | color: '#104b63', 43 | changed: function (v) { 44 | if (!delayNode) { return false } 45 | const delay = MaxDelay * v.deg / 360 46 | // Set it 47 | delayNode.delayTime.value = delay 48 | // UPdate delay 49 | document.getElementById('value').innerHTML = 'Delay: ' + delay.toFixed(3) + 's' 50 | } 51 | }) 52 | 53 | // Create audio context 54 | const audioContext = new window.AudioContext({ sampleRate: 48000 }) 55 | 56 | const tokenGenerator = () => Director.getSubscriber(streamName, streamAccountId) 57 | window.millicastView = millicastView = new View(undefined, tokenGenerator, null, true) 58 | millicastView.on('track', ({ track }) => { 59 | // Ignore non audio tracks 60 | if (track.kind !== 'audio') { return } 61 | // Create delay node 62 | delayNode = audioContext.createDelay(MaxDelay) 63 | // Create stream from track 64 | const stream = window.stream = new MediaStream([track]) 65 | 66 | // Chrome needs a dummy audio element to start pumping audio in the webaudio media soruce 67 | const audio = document.createElement('audio') 68 | audio.srcObject = stream 69 | audio.muted = true 70 | audio.play() 71 | 72 | // Create media source 73 | const source = audioContext.createMediaStreamSource(stream) 74 | 75 | // Creat primary graph, connect webrtc with the delay node and play it in the default destination 76 | source 77 | .connect(delayNode) 78 | .connect(audioContext.destination) 79 | // UPdate delay 80 | document.getElementById('value').innerHTML = 'Delay: 0s' 81 | // Enable pointer events 82 | document.getElementById('myCanvas').style['pointer-events'] = 'auto' 83 | }) 84 | // UPdate delay 85 | document.getElementById('value').innerHTML = '...connecting...' 86 | await millicastView.connect() 87 | } 88 | -------------------------------------------------------------------------------- /packages/millicast-multiview-demo/README.md: -------------------------------------------------------------------------------- 1 | # Millicast Multiview Demo 2 | 3 | The Multiview demo application demonstrates multi view playback capabilities that you can add to your application using the Millicast SDK. You can use it for rendering multiple real-time video and audio streams simultaneously inside a browser. 4 | 5 | 6 | 7 | ## Getting started 8 | 9 | 1. Go to the [Dolby.io Streaming dashboard](https://dashboard.dolby.io/) and select your publish token. If you do not have a token, create it by clicking the **create** button. 10 | 11 | 2. Open the **token details** tab and enable **multisource**. 12 | 13 | 3. In the same tab, locate and copy your `account ID`. 14 | 15 | 4. Select the **publishing** tab and copy your `stream name`. 16 | 17 | 5. Open the Millicast SDK in a code editor, create a `.env` file in the `millicast-multiview-demo` folder, and add the following data to the file: 18 | 19 | ```sh 20 | MILLICAST_STREAM_NAME=yourStreamName 21 | MILLICAST_ACCOUNT_ID=yourAccountId 22 | ``` 23 | 24 | This content is also available in the `.env.sample` file. 25 | 26 | 6. Replace `yourStreamName` and `yourAccountId` with the data copied from the dashboard. 27 | 28 | 7. Open a terminal in the `millicast-multiview-demo` folder. 29 | 30 | 8. Install all dependencies: 31 | ```sh 32 | npm ci 33 | ``` 34 | 9. Run the application: 35 | ```sh 36 | npm start 37 | ``` 38 | 39 | 10. Open `http://localhost:10005` and test the application. 40 | 41 | To receive streams, you need to broadcast them first. You can do it either via the Dolby.io dashboard by clicking the **broadcast** button, located next to your token, or you can use the [Publisher](../millicast-publisher-demo/) demo application. After you start broadcasting, the Multiview application will be able to play the streamed content. 42 | 43 | The simplest way to receive two streams in the application is to open the dashboard in two browser tabs and start broadcasting a stream from each tab. It additionally requires opening **media settings** via the gear icon and providing a **source ID** for each stream. The ID is a stream name that simplifies stream identification. 44 | 45 | The application also lets you select the preferred layer, which refers to a simulcast bitrate. This option is available only after enabling **simulcast** in the **media settings**. 46 | 47 | ## Custom connect options through URL parameters 48 | This demo application allows the user to set some URL parameters for configuring stream connection options: 49 | 50 | | Name | Description | Default value 51 | | --- | --- | --- | 52 | | **accountId** | Publisher's account ID | `null` 53 | | **streamName** | Publisher's stream name | `null` 54 | | **metadata** | Enable metadata extraction on all sources available | `false` 55 | | **disableVideo** | Set to disable video from the stream | `false` 56 | | **disableAudio** | Set to disable audio from the stream | `false` 57 | 58 | ## Introducing updates 59 | After introducing any changes to the `public` directory, use the following command: 60 | ``` 61 | npm run prepare 62 | ``` 63 | 64 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/LoggerHandlers.steps.js: -------------------------------------------------------------------------------- 1 | import { loadFeature, defineFeature } from 'jest-cucumber' 2 | import Logger from '../../src/Logger' 3 | const feature = loadFeature('../features/LoggerHandlers.feature', { loadRelativePath: true, errors: true }) 4 | 5 | defineFeature(feature, test => { 6 | test('Gets messages from same level', ({ given, when, then }) => { 7 | const handler = jest.fn() 8 | 9 | given('I set a custom handler at INFO level', async () => { 10 | Logger.setHandler(handler, Logger.INFO) 11 | }) 12 | 13 | when('I log a message at INFO level', async () => { 14 | Logger.info('This is a log message') 15 | }) 16 | 17 | then('I receive this message in handler', async () => { 18 | expect(handler).toHaveBeenCalledTimes(1) 19 | expect(handler).toHaveBeenCalledWith( 20 | expect.objectContaining({ 0: 'This is a log message' }), 21 | { level: Logger.INFO, filterLevel: Logger.TRACE } 22 | ) 23 | }) 24 | }) 25 | 26 | test('Gets messages from lower level', ({ given, when, then }) => { 27 | const handler = jest.fn() 28 | 29 | given('I set a custom handler at INFO level', async () => { 30 | Logger.setHandler(handler, Logger.INFO) 31 | }) 32 | 33 | when('I log a message at DEBUG level', async () => { 34 | Logger.debug('This is a log message') 35 | }) 36 | 37 | then('custom handler does not receive any message', async () => { 38 | expect(handler).not.toHaveBeenCalled() 39 | }) 40 | }) 41 | 42 | test('Gets messages from higher level', ({ given, when, then }) => { 43 | const handler = jest.fn() 44 | 45 | given('I set a custom handler at INFO level', async () => { 46 | Logger.setHandler(handler, Logger.INFO) 47 | }) 48 | 49 | when('I log a message at ERROR level', async () => { 50 | Logger.error('This is a log message') 51 | }) 52 | 53 | then('I receive this message in handler', async () => { 54 | expect(handler).toHaveBeenCalledTimes(1) 55 | expect(handler).toHaveBeenCalledWith( 56 | expect.objectContaining({ 0: 'This is a log message' }), 57 | { level: Logger.ERROR, filterLevel: Logger.TRACE } 58 | ) 59 | }) 60 | }) 61 | 62 | test('Multiple handlers', ({ given, when, then }) => { 63 | const infoHandler = jest.fn() 64 | const errorHandler = jest.fn() 65 | 66 | given('I set a custom handler at INFO level and other at ERROR level', async () => { 67 | Logger.setHandler(infoHandler, Logger.INFO) 68 | Logger.setHandler(errorHandler, Logger.ERROR) 69 | }) 70 | 71 | when('I log a message at ERROR level', async () => { 72 | Logger.error('This is a log message') 73 | }) 74 | 75 | then('both handlers receive this message', async () => { 76 | expect(infoHandler).toHaveBeenCalledTimes(1) 77 | expect(infoHandler).toHaveBeenCalledWith( 78 | expect.objectContaining({ 0: 'This is a log message' }), 79 | { level: Logger.ERROR, filterLevel: Logger.TRACE } 80 | ) 81 | 82 | expect(errorHandler).toHaveBeenCalledTimes(1) 83 | expect(errorHandler).toHaveBeenCalledWith( 84 | expect.objectContaining({ 0: 'This is a log message' }), 85 | { level: Logger.ERROR, filterLevel: Logger.TRACE } 86 | ) 87 | }) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /packages/millicast-publisher-demo/README.md: -------------------------------------------------------------------------------- 1 | # Millicast Publisher Demo 2 | 3 | The Publisher demo application demonstrates broadcasting capabilities that you can add to your application using the Millicast SDK. You can use it to experience and test streaming high-value content with ultra-low latency. The application allows selecting the preferred microphone, camera, and the maximum bitrate and starting broadcasting. 4 | 5 | 6 | 7 | We recommend using the application together with the [Viewer](../millicast-viewer-demo/) demo application to test the receiving side as well. 8 | 9 | ## Getting started 10 | 11 | 1. Go to the [Dolby.io Streaming dashboard](https://dashboard.dolby.io/) and select your publish token. If you do not have a token, create it by clicking the **create** button. 12 | 13 | 2. Locate your `account ID` in the **token details** tab and copy the token. 14 | 15 | 3. Select the **publishing** tab and copy your `publishing token` and `stream name`. 16 | 17 | 4. Open the Millicast SDK in a code editor, create a `.env` file in the `millicast-publisher-demo` folder, and add the following data to the file: 18 | 19 | ```sh 20 | MILLICAST_STREAM_NAME=yourStreamName 21 | MILLICAST_ACCOUNT_ID=yourAccountId 22 | MILLICAST_PUBLISH_TOKEN=yourPublishToken 23 | ``` 24 | 25 | This content is also available in the `.env.sample` file. 26 | 27 | 5. Replace `yourStreamName`, `yourAccountId`, and `yourPublishToken` with the data copied from the dashboard. 28 | 29 | 6. Open a terminal in the `millicast-publisher-demo` folder. 30 | 31 | 7. Install all dependencies: 32 | ```sh 33 | npm ci 34 | ``` 35 | 8. Run the application: 36 | ```sh 37 | npm start 38 | ``` 39 | 40 | 9. Open `http://localhost:10001` and test the application. 41 | 42 | ## Custom connect options through URL parameters 43 | This demo application allows the user to set some URL parameters for configuring stream connection options: 44 | 45 | | Name | Description | Default value 46 | | --- | --- | --- | 47 | | **metadata** | Enable metadata to be inserted by calling `sendMetadata` Publish method. | `false` 48 | | **sourceId** | Set source ID for multiview purposes. `null` source ID is main source. | `null` 49 | | **simulcast** | Set to enable simulcast support. Only available for Chromium browsers and H264/VP8 codecs. | `false` 50 | | **codec** | Set codec to publish the stream. Possible values are: `h264`, `h265`, `vp8`, `vp9`, `av1` depending on browser capabilities. | `h264` 51 | | **priority** | Set stream priority over other streams from the same account. Its decimal integer value ranges from `-2^31` to `+2^31 -1`. | `null` 52 | | **disableVideo** | Set to disable video for the stream. | `false` 53 | | **disableAudio** | Set to disable audio for the stream. | `false` 54 | 55 | ## Introducing updates 56 | After introducing any changes to the `public` directory, use the following command: 57 | ``` 58 | npm run prepare 59 | ``` 60 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/SdpStereo.steps.js: -------------------------------------------------------------------------------- 1 | import { loadFeature, defineFeature } from 'jest-cucumber' 2 | import SdpParser from '../../src/utils/SdpParser' 3 | const feature = loadFeature('../features/SdpStereo.feature', { loadRelativePath: true, errors: true }) 4 | 5 | defineFeature(feature, test => { 6 | test('Set stereo', ({ given, when, then }) => { 7 | let localSdp 8 | 9 | given('a local sdp without stereo', async () => { 10 | localSdp = 'v=0\r\no=- 1619467151495 1 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\na=msid-semantic: WMS\r\na=ice-lite\r\nm=audio 44505 UDP/TLS/RTP/SAVPF 111\r\nc=IN IP4 165.227.59.173\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:1 1 udp 2130706431 165.227.59.173 44505 typ host generation 0\r\na=ice-ufrag:0a537ad0ed093359\r\na=ice-pwd:3b08c9441658c7a72c7d969bbaf8bd0c2e43a5d3e5a40230\r\na=fingerprint:sha-256 20:FC:C7:73:DC:BE:E8:00:10:CB:09:03:23:B0:8E:D0:DA:ED:06:D7:E2:AA:D0:49:1F:78:45:61:65:6D:72:00\r\na=setup:passive\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=sendrecv\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:111 opus/48000/2\r\na=fmtp:111 minptime=10;useinbandfec=1\r\nm=video 44505 UDP/TLS/RTP/SAVPF 102 121 125 107 124 119 123 118\r\nc=IN IP4 165.227.59.173\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:1 1 udp 2130706431 165.227.59.173 44505 typ host generation 0\r\na=ice-ufrag:0a537ad0ed093359\r\na=ice-pwd:3b08c9441658c7a72c7d969bbaf8bd0c2e43a5d3e5a40230\r\na=fingerprint:sha-256 20:FC:C7:73:DC:BE:E8:00:10:CB:09:03:23:B0:8E:D0:DA:ED:06:D7:E2:AA:D0:49:1F:78:45:61:65:6D:72:00\r\na=setup:passive\r\na=mid:1\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\na=sendrecv\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:102 H264/90000\r\na=rtcp-fb:102 transport-cc\r\na=rtcp-fb:102 ccm fir\r\na=rtcp-fb:102 nack\r\na=rtcp-fb:102 nack pli\r\na=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\r\na=rtpmap:121 rtx/90000\r\na=fmtp:121 apt=102\r\na=rtpmap:125 H264/90000\r\na=rtcp-fb:125 transport-cc\r\na=rtcp-fb:125 ccm fir\r\na=rtcp-fb:125 nack\r\na=rtcp-fb:125 nack pli\r\na=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\na=rtpmap:107 rtx/90000\r\na=fmtp:107 apt=125\r\na=rtpmap:124 H264/90000\r\na=rtcp-fb:124 transport-cc\r\na=rtcp-fb:124 ccm fir\r\na=rtcp-fb:124 nack\r\na=rtcp-fb:124 nack pli\r\na=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f\r\na=rtpmap:119 rtx/90000\r\na=fmtp:119 apt=124\r\na=rtpmap:123 H264/90000\r\na=rtcp-fb:123 transport-cc\r\na=rtcp-fb:123 ccm fir\r\na=rtcp-fb:123 nack\r\na=rtcp-fb:123 nack pli\r\na=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f\r\na=rtpmap:118 rtx/90000\r\na=fmtp:118 apt=123\r\n' 11 | expect(localSdp).toEqual(expect.not.stringMatching('useinbandfec=1; stereo=1')) 12 | }) 13 | 14 | when('I want to set stereo', async () => { 15 | localSdp = SdpParser.setStereo(localSdp) 16 | }) 17 | 18 | then('returns the sdp with stereo support', async () => { 19 | expect(localSdp).toEqual(expect.stringMatching('useinbandfec=1; stereo=1')) 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /packages/millicast-publisher-demo/src/js/MillicastPublishUserMedia.js: -------------------------------------------------------------------------------- 1 | import {Publish} from "@millicast/sdk" 2 | import MillicastMedia from "./MillicastMedia" 3 | 4 | export default class MillicastPublishUserMedia extends Publish { 5 | constructor(options, tokenGenerator, autoReconnect) { 6 | super(options.streamName, tokenGenerator, autoReconnect); 7 | this.mediaManager = new MillicastMedia(options); 8 | } 9 | 10 | static async build(options, tokenGenerator, autoReconnect = true) { 11 | const instance = new MillicastPublishUserMedia(options, tokenGenerator, autoReconnect); 12 | await instance.getMediaStream(); 13 | return instance; 14 | } 15 | 16 | get constraints() { 17 | return this.mediaManager.constraints; 18 | } 19 | 20 | set constraints(constraints) { 21 | this.mediaManager.constraints = constraints; 22 | } 23 | 24 | get devices() { 25 | return this.mediaManager.getDevices; 26 | } 27 | 28 | get activeVideo() { 29 | return this.mediaManager.videoInput; 30 | } 31 | 32 | get activeAudio() { 33 | return this.mediaManager.audioInput; 34 | } 35 | 36 | async connect( 37 | options = { 38 | bandwidth: 0, 39 | disableVideo: false, 40 | disableAudio: false, 41 | } 42 | ) { 43 | await super.connect({ 44 | ...options, 45 | mediaStream: this.mediaManager.mediaStream 46 | }); 47 | 48 | this.webRTCPeer.on('stats', (stats) => { 49 | console.log(stats) 50 | }) 51 | } 52 | 53 | async getMediaStream() { 54 | try { 55 | return await this.mediaManager.getMedia(); 56 | } catch (e) { 57 | throw e; 58 | } 59 | } 60 | 61 | destroyMediaStream() { 62 | this.mediaManager.mediaStream = null; 63 | } 64 | 65 | updateMediaStream(type, id) { 66 | if (type === "audio") { 67 | return new Promise((resolve, reject) => { 68 | this.mediaManager 69 | .changeAudio(id) 70 | .then((stream) => { 71 | this.mediaManager.mediaStream = stream; 72 | if (this.isActive()) { 73 | this.webRTCPeer.replaceTrack(stream.getAudioTracks()[0]) 74 | } 75 | resolve(stream); 76 | }) 77 | .catch((error) => { 78 | console.error("Could not update Audio: ", error); 79 | reject(error); 80 | }); 81 | }); 82 | } else if (type === "video") { 83 | return new Promise((resolve, reject) => { 84 | this.mediaManager 85 | .changeVideo(id) 86 | .then((stream) => { 87 | this.mediaManager.mediaStream = stream; 88 | if (this.isActive()) { 89 | this.webRTCPeer.replaceTrack(stream.getVideoTracks()[0]) 90 | } 91 | resolve(stream); 92 | }) 93 | .catch((error) => { 94 | console.error("Could not update Video: ", error); 95 | reject(error); 96 | }); 97 | }); 98 | } else { 99 | return Promise.reject(`Invalid Type: ${type}`); 100 | } 101 | } 102 | 103 | sendMessage(message) { 104 | if (this.options.codec === 'h264') this.sendMetadata(message) 105 | } 106 | 107 | muteMedia(type, boo) { 108 | if (type === "audio") { 109 | return this.mediaManager.muteAudio(boo); 110 | } else if (type === "video") { 111 | return this.mediaManager.muteVideo(boo); 112 | } else { 113 | return false; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/SdpAbsCaptureTime.steps.js: -------------------------------------------------------------------------------- 1 | import { loadFeature, defineFeature } from 'jest-cucumber' 2 | import SdpParser from '../../src/utils/SdpParser' 3 | const feature = loadFeature('../features/SdpAbsCaptureTime.feature', { loadRelativePath: true, errors: true }) 4 | 5 | defineFeature(feature, test => { 6 | test('Set abs-capture-time', ({ given, when, then }) => { 7 | let localSdp 8 | 9 | given('a local sdp without the header extension', async () => { 10 | localSdp = 'v=0\r\no=- 1619467151495 1 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\na=msid-semantic: WMS\r\na=ice-lite\r\nm=audio 44505 UDP/TLS/RTP/SAVPF 111\r\nc=IN IP4 165.227.59.173\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:1 1 udp 2130706431 165.227.59.173 44505 typ host generation 0\r\na=ice-ufrag:0a537ad0ed093359\r\na=ice-pwd:3b08c9441658c7a72c7d969bbaf8bd0c2e43a5d3e5a40230\r\na=fingerprint:sha-256 20:FC:C7:73:DC:BE:E8:00:10:CB:09:03:23:B0:8E:D0:DA:ED:06:D7:E2:AA:D0:49:1F:78:45:61:65:6D:72:00\r\na=setup:passive\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=sendrecv\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:111 opus/48000/2\r\na=fmtp:111 minptime=10;useinbandfec=1\r\nm=video 44505 UDP/TLS/RTP/SAVPF 102 121 125 107 124 119 123 118\r\nc=IN IP4 165.227.59.173\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:1 1 udp 2130706431 165.227.59.173 44505 typ host generation 0\r\na=ice-ufrag:0a537ad0ed093359\r\na=ice-pwd:3b08c9441658c7a72c7d969bbaf8bd0c2e43a5d3e5a40230\r\na=fingerprint:sha-256 20:FC:C7:73:DC:BE:E8:00:10:CB:09:03:23:B0:8E:D0:DA:ED:06:D7:E2:AA:D0:49:1F:78:45:61:65:6D:72:00\r\na=setup:passive\r\na=mid:1\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\na=sendrecv\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:102 H264/90000\r\na=rtcp-fb:102 transport-cc\r\na=rtcp-fb:102 ccm fir\r\na=rtcp-fb:102 nack\r\na=rtcp-fb:102 nack pli\r\na=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\r\na=rtpmap:121 rtx/90000\r\na=fmtp:121 apt=102\r\na=rtpmap:125 H264/90000\r\na=rtcp-fb:125 transport-cc\r\na=rtcp-fb:125 ccm fir\r\na=rtcp-fb:125 nack\r\na=rtcp-fb:125 nack pli\r\na=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\na=rtpmap:107 rtx/90000\r\na=fmtp:107 apt=125\r\na=rtpmap:124 H264/90000\r\na=rtcp-fb:124 transport-cc\r\na=rtcp-fb:124 ccm fir\r\na=rtcp-fb:124 nack\r\na=rtcp-fb:124 nack pli\r\na=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f\r\na=rtpmap:119 rtx/90000\r\na=fmtp:119 apt=124\r\na=rtpmap:123 H264/90000\r\na=rtcp-fb:123 transport-cc\r\na=rtcp-fb:123 ccm fir\r\na=rtcp-fb:123 nack\r\na=rtcp-fb:123 nack pli\r\na=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f\r\na=rtpmap:118 rtx/90000\r\na=fmtp:118 apt=123\r\n' 11 | expect(localSdp).toEqual(expect.not.stringMatching('http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time')) 12 | }) 13 | 14 | when('I want to add header extension', async () => { 15 | localSdp = SdpParser.setAbsoluteCaptureTime(localSdp) 16 | }) 17 | 18 | then('returns the sdp with the header extension', async () => { 19 | expect(localSdp).toEqual(expect.stringMatching('http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time')) 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /packages/millicast-sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@millicast/sdk", 3 | "version": "0.6.1", 4 | "description": "SDK for building a realtime broadcaster using the Millicast platform.", 5 | "keywords": [ 6 | "sdk", 7 | "millicast", 8 | "webrtc", 9 | "realtime", 10 | "streaming" 11 | ], 12 | "main": "dist/millicast.js", 13 | "module": "dist/millicast.mjs", 14 | "browser": "dist/millicast.umd.js", 15 | "millicastdebug": "dist/millicast.debug.umd.js", 16 | "types": "dist/millicast.d.ts", 17 | "files": [ 18 | "dist", 19 | "scripts" 20 | ], 21 | "scripts": { 22 | "build": "tsc --build && vite build --config vite.config.debug.mjs && vite build && rollup -c", 23 | "build:watch": "vite build --watch", 24 | "build-docs": "jsdoc -c jsdoc.json -R ../../README.md", 25 | "start-docs": "npm run build-docs && serve docs", 26 | "test-unit": "npm run build && jest --testMatch \"**/unit/*.steps.js\"", 27 | "test-unit-coverage": "npm run build && jest --testMatch \"**/unit/*.steps.js\" --coverage", 28 | "test-e2e": "npm run build && jest --testMatch \"**/e2e/*.steps.js\" --verbose", 29 | "test-all": "npm run build && jest --testMatch \"**/*.steps.js\"", 30 | "test": "npm run test-all" 31 | }, 32 | "babel": { 33 | "env": { 34 | "test": { 35 | "presets": [ 36 | [ 37 | "@babel/preset-env", 38 | { 39 | "targets": { 40 | "node": 12 41 | } 42 | } 43 | ] 44 | ] 45 | } 46 | } 47 | }, 48 | "author": "Millicast, Inc.", 49 | "homepage": "https://github.com/millicast/millicast-sdk#readme", 50 | "license": "See in LICENSE file", 51 | "repository": { 52 | "type": "git", 53 | "url": "git+https://github.com/millicast/millicast-sdk.git" 54 | }, 55 | "dependencies": { 56 | "@dolbyio/webrtc-stats": "^1.0.4", 57 | "@types/node": "^18.11.10", 58 | "Base64": "^1.1.0", 59 | "buffer": "^6.0.3", 60 | "events": "^3.3.0", 61 | "js-logger": "^1.6.1", 62 | "jsdoc-i18n-plugin": "^0.0.3", 63 | "jwt-decode": "^3.1.2", 64 | "re-emitter": "^1.1.4", 65 | "semantic-sdp": "^3.22.0", 66 | "transaction-manager": "^2.1.3", 67 | "ua-parser-js": "^0.7.30", 68 | "valibot": "^1.1.0" 69 | }, 70 | "devDependencies": { 71 | "@babel/core": "^7.23.6", 72 | "@babel/helpers": "^7.13.10", 73 | "@babel/plugin-transform-modules-commonjs": "^7.13.8", 74 | "@babel/plugin-transform-runtime": "^7.13.10", 75 | "@babel/preset-env": "^7.23.6", 76 | "@babel/runtime": "^7.13.10", 77 | "@rollup/plugin-babel": "^6.0.4", 78 | "@rollup/plugin-commonjs": "^28.0.3", 79 | "@rollup/plugin-json": "^6.1.0", 80 | "@rollup/plugin-node-resolve": "^16.0.1", 81 | "babel-jest": "^29.7.0", 82 | "clean-jsdoc-theme": "^4.2.17", 83 | "core-js": "^3.20.1", 84 | "dotenv": "^16.5.0", 85 | "jest": "^30.0.0", 86 | "jest-cucumber": "^4.5.0", 87 | "jest-environment-jsdom": "^30.0.0", 88 | "jest-puppeteer": "^11.0.0", 89 | "jest-websocket-mock": "^2.5.0", 90 | "jsdoc": "^4.0.2", 91 | "jsdoc-export-default-interop": "^0.3.1", 92 | "mock-socket": "^9.0.3", 93 | "puppeteer": "^24.10.1", 94 | "rollup": "4.43.0", 95 | "rollup-plugin-cleanup": "^3.2.1", 96 | "rollup-plugin-dts": "^6.2.1", 97 | "rollup-plugin-filesize": "^10.0.0", 98 | "rollup-plugin-terser": "^7.0.2", 99 | "serve": "^14.2.1", 100 | "typescript": "^5.8.3", 101 | "underscore": "^1.13.1", 102 | "vite": "^6.3.5" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/features/OfferPublishingStream.feature: -------------------------------------------------------------------------------- 1 | Feature: As a user I want to signal Millicast Server so I can offer publishing a stream 2 | 3 | Scenario: Offer a SDP with no previous connection and h264 codec 4 | Given a local sdp and no previous connection to server 5 | When I offer my local sdp with h264 codec and recording option 6 | Then returns a filtered sdp to offer to remote peer 7 | 8 | Scenario: Offer a SDP with no previous connection and vp8 codec 9 | Given a local sdp and no previous connection to server 10 | When I offer my local sdp with vp8 codec 11 | Then returns a filtered sdp to offer to remote peer 12 | 13 | Scenario: Offer a SDP with no previous connection and vp9 codec 14 | Given a local sdp and no previous connection to server 15 | When I offer my local sdp with vp9 codec 16 | Then returns a filtered sdp to offer to remote peer 17 | 18 | Scenario: Offer a SDP with no previous connection and av1 codec and browser supports av1x 19 | Given a local sdp and no previous connection to server 20 | When I offer my local sdp with av1 codec 21 | Then returns a filtered sdp to offer to remote peer 22 | 23 | Scenario: Offer a SDP with no previous connection and av1 codec and browser supports av1 24 | Given a local sdp and no previous connection to server 25 | When I offer my local sdp with av1 codec 26 | Then returns a filtered sdp to offer to remote peer 27 | 28 | Scenario: Offer a SDP with no previous connection and av1 codec and browser does not have getCapabilities 29 | Given a local sdp and no previous connection to server 30 | When I offer my local sdp with av1 codec 31 | Then returns a filtered sdp to offer to remote peer 32 | 33 | Scenario: Offer a SDP with no previous connection and options as object 34 | Given a local sdp and no previous connection to server 35 | When I offer my local sdp using options object 36 | Then returns a filtered sdp to offer to remote peer 37 | 38 | Scenario: Offer a SDP with previous connection and h264 codec 39 | Given a local sdp and a previous active connection to server 40 | When I offer my local spd with h264 codec 41 | Then returns a filtered sdp to offer to remote peer 42 | 43 | Scenario: Offer no SDP with no previous connection 44 | Given I have not previous connection to server 45 | When I offer a null sdp 46 | Then throws no sdp error 47 | 48 | Scenario: Offer no SDP with previous connection 49 | Given I have previous connection to server 50 | When I offer a null sdp 51 | Then throws no sdp error 52 | 53 | Scenario: Offer a SDP with unexistent stream name 54 | Given I have not previous connection to server 55 | When I offer my local spd and an unexistent stream name 56 | Then returns a filtered sdp to offer to remote peer 57 | 58 | Scenario: Offer SDP without stream with no previous connection 59 | Given I have not previous connection to server 60 | When I offer a sdp without stream 61 | Then throws no stream found error 62 | 63 | Scenario: Offer a SDP with invalid codec 64 | Given I have not previous connection to server 65 | When I offer a sdp with invalid codec 66 | Then throws no valid codec error 67 | 68 | Scenario: Offer a SDP with no codec 69 | Given I have not previous connection to server 70 | When I offer a sdp 71 | Then returns a filtered sdp to offer to remote peer 72 | 73 | Scenario: Offer a SDP with no previous connection and desired events 74 | Given a local sdp and no previous connection to server 75 | When I offer my local sdp and I set the events active and inactive as events that i want to get 76 | Then returns a filtered sdp to offer to remote peer -------------------------------------------------------------------------------- /packages/millicast-viewer-demo/README.md: -------------------------------------------------------------------------------- 1 | # Millicast Viewer Demo 2 | 3 | The Viewer demo application demonstrates playback capability that you can add to your application using the Millicast SDK. You can use it to experience and test receiving high-value content with ultra-low latency. 4 | 5 | ## Getting started 6 | 7 | 1. Go to the [Dolby.io Streaming dashboard](https://dashboard.dolby.io/) and select your publish token. If you do not have a token, create it by clicking the **create** button. 8 | 9 | 2. Locate your `account ID` in the **token details** tab and copy the token. 10 | 11 | 3. Select the **publishing** tab and copy your `stream name`. 12 | 13 | 4. Open the Millicast SDK in a code editor, create a `.env` file in the `millicast-viewer-demo` folder, and add the following data to the file: 14 | 15 | ```sh 16 | MILLICAST_STREAM_NAME=yourStreamName 17 | MILLICAST_ACCOUNT_ID=yourAccountId 18 | ``` 19 | 20 | This content is also available in the `.env.sample` file. 21 | 22 | 5. Replace `yourStreamName` and `yourAccountId` with the data copied from the dashboard. 23 | 24 | 6. Open a terminal in the `millicast-viewer-demo` folder. 25 | 26 | 7. Install all dependencies: 27 | ```sh 28 | npm ci 29 | ``` 30 | 8. Run the application: 31 | ```sh 32 | npm start 33 | ``` 34 | 35 | 9. Open `http://localhost:10002` and test the application. 36 | 37 | To receive a stream, you need to broadcast it first. You can do it either via the Dolby.io dashboard by clicking the **broadcast** button, located next to your token, or you can use the [Publisher](../millicast-publisher-demo/) demo application. After you start broadcasting, the Viewer application will be able to play the streamed content. 38 | 39 | ## Custom connect options through URL parameters 40 | This demo application allows the user to set some URL parameters for configuring stream connection options: 41 | 42 | | Name | Description | Default value 43 | | --- | --- | --- | 44 | | **url** | WebSocket URL | `wss://turn.millicast.com/millisock` 45 | | **accountId** | Publisher's account ID. | `null` 46 | | **streamName** | Publisher's stream name. | `null` 47 | | **metadata** | Enable metadata extraction. | `false` 48 | | **disableVideo** | Set to disable video from the stream. | `false` 49 | | **disableAudio** | Set to disable audio from the stream. | `false` 50 | | **muted** | Set to mute the video player at first connection. | `true` 51 | | **autoplay** | Set to play the video at first connection. | `true` 52 | | **autoReconnect** | Set to enable auto reconnection. | `true` 53 | | **disableControls** | Set to disable video player controls, such us volume, fullscreen, play/pause. | `false` 54 | | **disableVolume** | Set to disable volume control. | `false` 55 | | **disablePlay** | Set to disable play/pause control. | `false` 56 | | **disableFull** | Set to disable fullscreen control. | `false` 57 | 58 | ## Introducing updates 59 | After introducing any changes to the `public` directory, use the following command: 60 | ``` 61 | npm run prepare 62 | ``` 63 | 64 | -------------------------------------------------------------------------------- /packages/millicast-sdk/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @millicast/sdk 2 | 3 | ## 0.6.1 4 | 5 | ### Patch Changes 6 | 7 | - 4f37a56: Allowed overrides of director URL to persist 8 | 9 | ## 0.6.0 10 | 11 | ### Minor Changes 12 | 13 | - 576652c: Added support for ABR strategy and iniital bitrate for the viewer. 14 | 15 | ## 0.5.0 16 | 17 | ### Minor Changes 18 | 19 | - 6304c52: Added forceSmooth to viewer connect options. 20 | 21 | ### Patch Changes 22 | 23 | - 8b24ea1: Improved support for frame metadata extraction on older browsers 24 | - 4dfc848: Optimized the bundle size. 25 | - 6304c52: Removed `onMetadata` event which was already deprecated. This has been superceded by `metadata` as of v0.3.0 26 | 27 | ## 0.4.0 28 | 29 | ### Minor Changes 30 | 31 | - 5d25d25: Aligning types for sourceId property within project method and global types .d.ts file 32 | Added `metadata` event to documentation 33 | Updated DRM SDK and suppressed DRM verbose logging. 34 | Updated `@dolbyio/webrtc-stats` to newer version. 35 | 36 | ## 0.3.2 37 | 38 | ### Patch Changes 39 | 40 | - Revert View mediaElement being removed 41 | 42 | ## 0.3.1 43 | 44 | ### Patch Changes 45 | 46 | - e84dc75: Fixed build issue with Vite 47 | 48 | ## 0.3.0 49 | 50 | ### Minor Changes 51 | 52 | - 43582cf: Added DRM support 53 | 54 | ### Patch Changes 55 | 56 | - 1e19119: Fixed the excessive waiting time to resume the video playback when stream beomes active from inactive 57 | - 2a57672: Fix security vulnerabilities 58 | - 9d54c92: fixed bug: no video when the main source id is not null 59 | - cff3555: Avoid connecting when A+V are disabled 60 | 61 | ## 0.2.1 62 | 63 | ### Patch Changes 64 | 65 | - 635e55e: Added bitrateBitsPerSecond stats attribute. Now bitrate attribute is shown in Bytes per second and bitrateBitsPerSecond in bits per second. 66 | - 1d7fc65: Add connection duration to timestamp. 67 | - 405861e: Fix security vulnerabilities. 68 | - 22b150f: Metadata UUID is now optional. 69 | 70 | ## 0.2.0 71 | 72 | ### Minor Changes 73 | 74 | - d996963: Allow the user to configure the stats timeout value 75 | - 332b9bf: Improve build time performance 76 | - ec98d00: add hot reload in sdk 77 | 78 | ### Patch Changes 79 | 80 | - 955d1c3: Let the browser determine the available codecs on Firefox 81 | - ff4f101: Deprecate the old UUID and use a new one for messages that include a timecode 82 | - ea41a2e: When enabling simulcast, first check if there is video in the SDP payload 83 | - ff4f101: Add in a timecode for SDK generated messages as well 84 | - ff4f101: update the metadata event triggered to be \'metadata\' instead of \'onMetadata\' 85 | - 11fe87d: Block viewer from trying to update the bitrate, because it\'s not a permitted operation 86 | 87 | ## 0.1.46 88 | 89 | ### Patch Changes 90 | 91 | - 448bc44: New 'history' property added to the diagnose method response which contains recent events. Two new diagnose parameters, 'historySize' and 'minLogLevel', added to customize diagnose method history property. 92 | - 86feed2: Update docs allowing simulcast to all Chromium based browsers 93 | - ed91010: Added MILLICAST_DIRECTOR_ENDPOINT as a environment variable for demos apps to set Director's endpoint as pleased 94 | - 5bd8ef5: Stats are now initialized by default. Logger.diagnose method changed to accept an object as parameter, with statsCount, historySize, minLogLevel, statsFormat. 95 | - faef94f: Deprecated the streamName argument in publish and view 96 | - 3bc547c: Added SEI user unregistered data extraction and insertion for H.264 codec. 97 | - 661f150: Added streamViewId variable in Signaling instance when subscribing to a stream. 98 | 99 | ## 0.1.44 100 | 101 | ### Patch Changes 102 | 103 | - 4149116: added missed layer info in view command 104 | - 4a44f4b: Added Logger.diagnose(statsCount) function to get useful debugging information 105 | - 4a44f4b: Added changesets for changelog control 106 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/e2e/Publish.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 31 | 32 |
33 |

Publish

34 |
35 |

READY!

36 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 55 | 56 | 57 | 60 | 63 | 64 |
PublisherViewer
43 | 44 | 45 | 48 | 51 | 53 | 54 |
58 | 59 | 61 |
62 |
65 |
66 |
67 | Codec: 68 | Scalability mode: 69 | 70 | 71 | Bitrate: 79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 |
87 | 90 | 93 | 96 | 99 |
100 |
101 |
102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 |
MediaTrackIdCodecFrame WidthFrame HeightQuality limitation reasonFPSBytes sent (total)Bitrate (kbps)Timestamp
119 |
120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 |
Candidate typeAvailable outgoing bitrate (kbps)Current RTT (sec.)Total RTT (sec.)
130 |
131 |
132 | -------------------------------------------------------------------------------- /packages/millicast-sdk/src/utils/Diagnostics.js: -------------------------------------------------------------------------------- 1 | import { version } from '../../package.json' with { type: 'json' } 2 | 3 | const MAX_STATS_HISTORY_SIZE = 60 4 | 5 | const userAgent = window?.navigator?.userAgent || 'No user agent available' 6 | let _accountId = '' 7 | let _streamName = '' 8 | let _subscriberId = '' 9 | let _streamViewId = '' 10 | let _feedId = '' 11 | let _connection = '' 12 | let _cluster = '' 13 | let _connectionTime = 0 14 | const _stats = [] 15 | 16 | function transformWebRTCStatsToCMCD (diagnostics) { 17 | // Helper function to map individual stat objects to CMCD-like structure 18 | function mapStats (type, stat) { 19 | return { 20 | ts: Math.round(stat.timestamp) || '', // Timestamp to the nearest millisecond 21 | ot: type === 'audio' ? 'a' : 'v', // 'a' for audio, 'v' for video 22 | bl: stat.jitterBufferDelay || 0, // Buffer length from jitterBufferDelay, default to 0 if not available 23 | br: Math.round(stat.bitrateBitsPerSecond || 0), // Bitrate, rounded to nearest integer, default to 0 if not available 24 | pld: stat.packetsLostDeltaPerSecond || 0, // Packets lost delta per second, default to 0 if not available 25 | j: stat.jitter || 0, // Jitter, default to 0 if not available 26 | mtp: stat.packetRate || 0, // Measured throughput, approximated by packet rate, default to 0 if not available 27 | mid: stat.mid || '', // Media ID or track identifier, default to empty string if not available 28 | mimeType: stat.mimeType || '' // MIME type of the media stream, default to empty string if not available 29 | } 30 | } 31 | 32 | diagnostics.stats = diagnostics.stats.reduce((acc, stat) => { 33 | const audioStats = stat.audio.inbounds.length !== 0 34 | ? stat.audio.inbounds.map(statAudio => mapStats('audio', statAudio)) 35 | : stat.audio.outbounds.map(statAudio => mapStats('audio', statAudio)) 36 | const videoStats = stat.video.inbounds.length !== 0 37 | ? stat.video.inbounds.map(statVideo => mapStats('video', statVideo)) 38 | : stat.video.outbounds.map(statVideo => mapStats('video', statVideo)) 39 | 40 | return acc.concat([...audioStats, ...videoStats]) 41 | }, []) 42 | 43 | return diagnostics 44 | } 45 | 46 | const Diagnostics = { 47 | initAccountId: (accountId) => { _accountId = _accountId === '' ? accountId : _accountId }, 48 | initStreamName: (streamName) => { _streamName = _streamName === '' ? streamName : _streamName }, 49 | initSubscriberId: (subscriberId) => { _subscriberId = _subscriberId === '' ? subscriberId : _subscriberId }, 50 | initStreamViewId: (streamViewId) => { _streamViewId = _streamViewId === '' ? streamViewId : _streamViewId }, 51 | initFeedId: (feedId) => { _feedId = _feedId === '' ? feedId : _feedId }, 52 | setConnectionTime: (connectionTime) => { _connectionTime = _connectionTime === 0 ? connectionTime : _connectionTime }, 53 | setConnectionState: (connectionState) => { _connection = connectionState }, 54 | setClusterId: (clusterId) => { _cluster = _cluster === '' ? clusterId : _cluster }, 55 | 56 | addStats: (stats) => { 57 | if (_stats.length === MAX_STATS_HISTORY_SIZE) { 58 | _stats.shift() 59 | } 60 | _stats.push(stats) 61 | }, 62 | 63 | get: (statsCount = MAX_STATS_HISTORY_SIZE, statsFormat = 'JSON') => { 64 | let configuredStatsCount 65 | if (!Number.isInteger(statsCount) || statsCount > MAX_STATS_HISTORY_SIZE || statsCount <= 0) { 66 | configuredStatsCount = MAX_STATS_HISTORY_SIZE 67 | } else { 68 | configuredStatsCount = statsCount 69 | } 70 | 71 | const diagnostics = { 72 | client: '@millicast/millicast-sdk', 73 | version, 74 | timestamp: new Date().toISOString(), 75 | userAgent, 76 | clusterId: _cluster, 77 | accountId: _accountId, 78 | streamName: _streamName, 79 | subscriberId: _subscriberId, 80 | connection: _connection, 81 | stats: _stats.slice(-configuredStatsCount), 82 | connectionDurationMs: (new Date().getTime() - _connectionTime) 83 | } 84 | 85 | if (_feedId !== '') { 86 | diagnostics.feedId = _feedId 87 | } else if (_streamViewId !== '') { 88 | diagnostics.streamViewId = _streamViewId 89 | } 90 | if (statsFormat === 'CMCD') { 91 | return transformWebRTCStatsToCMCD(diagnostics) 92 | } 93 | 94 | return diagnostics 95 | } 96 | } 97 | 98 | export default Diagnostics 99 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/ManageSignaling.steps.js: -------------------------------------------------------------------------------- 1 | import { loadFeature, defineFeature } from 'jest-cucumber' 2 | import WS from 'jest-websocket-mock' 3 | import Signaling from '../../src/Signaling' 4 | import './__mocks__/MockBrowser' 5 | import { WebSocket } from 'mock-socket' 6 | const feature = loadFeature('../features/ManageSignaling.feature', { loadRelativePath: true, errors: true }) 7 | 8 | global.WebSocket = WebSocket 9 | 10 | defineFeature(feature, test => { 11 | const publishWebSocketLocation = 'ws://localhost:8080' 12 | const streamName = 'My Stream Name' 13 | let server = null 14 | let signaling = null 15 | const handler = jest.fn() 16 | 17 | beforeEach(async () => { 18 | server = new WS(publishWebSocketLocation, { jsonProtocol: true }) 19 | signaling = new Signaling({ 20 | streamName, 21 | url: publishWebSocketLocation 22 | }) 23 | }) 24 | 25 | afterEach(async () => { 26 | WS.clean() 27 | server = null 28 | signaling = null 29 | }) 30 | 31 | test('Connect to existing server with no errors', ({ given, when, then }) => { 32 | given('I have no previous connection to server', () => null) 33 | 34 | when('I want to connect to server', async () => { 35 | signaling.on('wsConnectionSuccess', handler) 36 | await signaling.connect() 37 | }) 38 | 39 | then('returns the WebSocket connection and fires a connectionSuccess event', async () => { 40 | expect(handler).toHaveBeenCalledTimes(1) 41 | expect(handler).toHaveBeenCalledWith({ ws: expect.any(WebSocket), tm: expect.any(Object) }) 42 | }) 43 | }) 44 | 45 | test('Connect again to existing server with no errors', ({ given, when, then }) => { 46 | given('I have a previous connection to server', async () => { 47 | signaling.on('wsConnectionSuccess', handler) 48 | await signaling.connect() 49 | }) 50 | 51 | when('I want to connect to server', async () => { 52 | await signaling.connect() 53 | }) 54 | 55 | then('returns the WebSocket connection and fires a connectionSuccess event', () => { 56 | expect(handler).toHaveBeenCalledTimes(2) 57 | expect(handler).toHaveBeenCalledWith({ ws: expect.any(WebSocket), tm: expect.any(Object) }) 58 | }) 59 | }) 60 | 61 | test('Connect to existing server with network errors', ({ given, when, then }) => { 62 | given('I have no previous connection to server', () => null) 63 | 64 | when('I want to connect to no responding server', async () => { 65 | server.on('connection', () => server.error()) 66 | const signaling = new Signaling({ 67 | streamName, 68 | url: publishWebSocketLocation 69 | }) 70 | signaling.on('wsConnectionError', handler) 71 | await signaling.connect() 72 | }) 73 | 74 | then('fires a connectionError event', () => { 75 | expect(handler).toHaveBeenCalledTimes(1) 76 | expect(handler).toHaveBeenCalledWith(expect.stringMatching(publishWebSocketLocation)) 77 | }) 78 | }) 79 | 80 | test('Receive broadcast events from server', ({ given, when, then }) => { 81 | given('I am connected to server', async () => { 82 | signaling.on('broadcastEvent', handler) 83 | await signaling.connect() 84 | }) 85 | 86 | when('the server send a broadcast event', () => { 87 | server.send({ type: 'event', name: 'active', data: { streamId: 'streamId' } }) 88 | }) 89 | 90 | then('fires a broadcastEvent event', () => { 91 | expect(handler).toHaveBeenCalledTimes(1) 92 | expect(handler).toHaveBeenCalledWith({ name: 'active', data: { streamId: 'streamId' }, namespace: undefined }) 93 | }) 94 | }) 95 | 96 | test('Close existing server connection', ({ given, when, then }) => { 97 | given('I am connected to server', async () => { 98 | await signaling.connect() 99 | }) 100 | 101 | when('I want to close connection', async () => { 102 | signaling.on('wsConnectionClose', handler) 103 | signaling.close() 104 | }) 105 | 106 | then('the connection closes', async () => { 107 | await server.closed 108 | expect(handler).toHaveBeenCalledTimes(1) 109 | expect(signaling.webSocket).toBe(null) 110 | }) 111 | }) 112 | 113 | test('Close unexisting server connection', ({ given, when, then }) => { 114 | given('I am not connected to server', () => null) 115 | 116 | when('I want to close connection', () => { 117 | signaling.close() 118 | }) 119 | 120 | then('websocket is not intitialized', () => { 121 | expect(signaling.webSocket).toBe(null) 122 | }) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/unit/UpdateBitrateWebRTC.steps.js: -------------------------------------------------------------------------------- 1 | import { loadFeature, defineFeature } from 'jest-cucumber' 2 | import PeerConnection, { ConnectionType } from '../../src/PeerConnection' 3 | import './__mocks__/MockMediaStream' 4 | import './__mocks__/MockRTCPeerConnection' 5 | import { changeBrowserMock } from './__mocks__/MockBrowser' 6 | const feature = loadFeature('../features/UpdateBitrateWebRTC.feature', { loadRelativePath: true, errors: true }) 7 | 8 | defineFeature(feature, test => { 9 | const config = { autoInitStats: true } 10 | afterEach(async () => { 11 | jest.restoreAllMocks() 12 | changeBrowserMock('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36') 13 | }) 14 | 15 | test('Update bitrate with restrictions', ({ given, when, then }) => { 16 | const peerConnection = new PeerConnection() 17 | const sdp = 'My default SDP' 18 | 19 | given('I have a peer connected', async () => { 20 | await peerConnection.createRTCPeer(config, ConnectionType.Publisher) 21 | await peerConnection.setRTCRemoteSDP(sdp) 22 | expect(peerConnection.peer.currentRemoteDescription.sdp).toBe(sdp) 23 | }) 24 | 25 | when('I want to update the bitrate to 1000 kbps', async () => { 26 | await peerConnection.updateBitrate(1000) 27 | }) 28 | 29 | then('the bitrate is updated', async () => { 30 | expect(peerConnection.peer.currentRemoteDescription.sdp).not.toBe(sdp) 31 | }) 32 | }) 33 | 34 | test('Update bitrate with no restrictions', ({ given, when, then }) => { 35 | const peerConnection = new PeerConnection() 36 | const sdp = 'My default SDP' 37 | 38 | given('I have a peer connected', async () => { 39 | await peerConnection.createRTCPeer(config, ConnectionType.Publisher) 40 | await peerConnection.setRTCRemoteSDP(sdp) 41 | expect(peerConnection.peer.currentRemoteDescription.sdp).toBe(sdp) 42 | }) 43 | 44 | when('I want to update the bitrate to unlimited', async () => { 45 | await peerConnection.updateBitrate() 46 | }) 47 | 48 | then('the bitrate is updated', async () => { 49 | expect(peerConnection.peer.currentRemoteDescription.sdp).not.toBe(sdp) 50 | }) 51 | }) 52 | 53 | test('Update bitrate with restrictions in Firefox', ({ given, when, then }) => { 54 | const peerConnection = new PeerConnection() 55 | const sdp = 'My default SDP' 56 | 57 | given('I am using Firefox and I have a peer connected', async () => { 58 | changeBrowserMock('Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0') 59 | await peerConnection.createRTCPeer(config, ConnectionType.Publisher) 60 | await peerConnection.setRTCRemoteSDP(sdp) 61 | expect(peerConnection.peer.currentRemoteDescription.sdp).toBe(sdp) 62 | }) 63 | 64 | when('I want to update the bitrate to 1000 kbps', async () => { 65 | await peerConnection.updateBitrate(1000) 66 | }) 67 | 68 | then('the bitrate is updated', async () => { 69 | expect(peerConnection.peer.currentRemoteDescription.sdp).not.toBe(sdp) 70 | }) 71 | }) 72 | 73 | test('Update bitrate with no existing peer', ({ given, when, then }) => { 74 | const peerConnection = new PeerConnection() 75 | let errorMessage 76 | 77 | given('I do not have a peer connected', async () => {}) 78 | 79 | when('I want to update the bitrate to 1000 kbps', async () => { 80 | try { 81 | await peerConnection.updateBitrate(1000) 82 | } catch (error) { 83 | errorMessage = error.message 84 | } 85 | }) 86 | 87 | then('throw no existing peer error', async () => { 88 | expect(errorMessage).toBe('Cannot update bitrate. No peer found.') 89 | }) 90 | }) 91 | 92 | test('Check update bitrate throws exception when in Viewer mode', ({ given, when, then }) => { 93 | const peerConnection = new PeerConnection() 94 | const sdp = 'My default SDP' 95 | let errorMessage 96 | 97 | given('I have a peer connected as a viewer', async () => { 98 | await peerConnection.createRTCPeer(config, ConnectionType.Viewer) 99 | await peerConnection.setRTCRemoteSDP(sdp) 100 | }) 101 | 102 | when('I want to update the bitrate to 1000 kbps', async () => { 103 | try { 104 | await peerConnection.updateBitrate(1000) 105 | } catch (error) { 106 | errorMessage = error.message 107 | } 108 | }) 109 | 110 | then('I get an exception', async () => { 111 | expect(errorMessage).toBe('It is not possible for a viewer to update the bitrate.') 112 | }) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/e2e/ViewTest.js: -------------------------------------------------------------------------------- 1 | const millicast = window.millicast 2 | 3 | const accountId = window.accountId 4 | const streamName = window.streamName 5 | 6 | class MillicastViewTest { 7 | constructor () { 8 | millicast.Logger.setLevel(millicast.Logger.DEBUG) 9 | millicast.Director.setEndpoint(window.directorEndpoint) 10 | const href = new URL(window.location.href) 11 | this.streamAccountId = (href.searchParams.get('streamAccountId')) ? href.searchParams.get('streamAccountId') : accountId 12 | this.streamName = (href.searchParams.get('streamName')) ? href.searchParams.get('streamName') : streamName 13 | this.playing = false 14 | this.disableVideo = false 15 | this.disableAudio = false 16 | const tokenGenerator = () => millicast.Director.getSubscriber({ streamName: this.streamName, streamAccountId: this.streamAccountId }) 17 | this.millicastView = new millicast.View(undefined, tokenGenerator) 18 | this.tracks = [] 19 | } 20 | 21 | async init () { 22 | this.subscribe() 23 | } 24 | 25 | async subscribe () { 26 | try { 27 | this.millicastView.on('track', (event) => { 28 | this.tracks.push(event) 29 | console.log('Event from newTrack: ', event) 30 | this.addStreamToVideoTag(event) 31 | }) 32 | this.millicastView.on('broadcastEvent', (event) => { 33 | console.log('Event from broadcastEvent: ', event) 34 | }) 35 | 36 | const options = { 37 | disableVideo: this.disableVideo, 38 | disableAudio: this.disableAudio, 39 | absCaptureTime: true 40 | } 41 | this.millicastView.on('connectionStateChange', (state) => { 42 | console.log('Event from connectionStateChange: ', state) 43 | }) 44 | await this.millicastView.connect(options) 45 | 46 | this.millicastView.webRTCPeer.initStats() 47 | this.millicastView.webRTCPeer.on('stats', (stats) => { 48 | this.stats = stats 49 | this.loadStatsInTable(stats) 50 | }) 51 | } catch (error) { 52 | console.log('There was an error while trying to connect with the publisher') 53 | this.millicastView.reconnect() 54 | } 55 | } 56 | 57 | testMigrate () { 58 | this.millicastView.signaling.emit('migrate') 59 | } 60 | 61 | addStreamToVideoTag (event) { 62 | this.addStream(event.streams[0]) 63 | } 64 | 65 | addStream (stream) { 66 | this.playing = true 67 | const video = document.getElementById('millicast-view-test') 68 | 69 | if (this.disableVideo) { 70 | if (video)video.parentNode.removeChild(video) 71 | } else { 72 | video.srcObject = stream 73 | } 74 | } 75 | 76 | loadStatsInTable (stats) { 77 | const candidateInfo = document.getElementById('candidate-info') 78 | candidateInfo.innerHTML = ` 79 | 80 | ${stats.candidateType} 81 | ${stats.currentRoundTripTime} 82 | ${stats.totalRoundTripTime} 83 | 84 | ` 85 | 86 | const tracksInfo = document.getElementById('tracks-info') 87 | const tracks = [] 88 | 89 | for (const track of stats.video.inbounds) { 90 | tracks.push(` 91 | 92 | Video 93 | ${track.mimeType} 94 | ${track.frameWidth} 95 | ${track.frameHeight} 96 | ${track.framesPerSecond} 97 | ${track.totalBytesReceived} 98 | ${track.packetsLostDeltaPerSecond} 99 | ${track.packetsLostRatioPerSecond} 100 | ${track.totalPacketsLost} 101 | ${track.jitter} 102 | ${track.bitrateBitsPerSecond / 1000} 103 | ${new Date(track.timestamp).toISOString()} 104 | 105 | `) 106 | } 107 | 108 | for (const track of stats.audio.inbounds) { 109 | tracks.push(` 110 | 111 | Audio 112 | ${track.mimeType} 113 | - 114 | - 115 | - 116 | ${track.totalBytesReceived} 117 | ${track.packetsLostDeltaPerSecond} 118 | ${track.packetsLostRatioPerSecond} 119 | ${track.totalPacketsLost} 120 | ${track.jitter} 121 | ${track.bitrateBitsPerSecond / 1000} 122 | ${new Date(track.timestamp).toISOString()} 123 | 124 | `) 125 | } 126 | 127 | tracksInfo.innerHTML = tracks.join(' ') 128 | } 129 | } 130 | 131 | const millicastViewTest = new MillicastViewTest() 132 | millicastViewTest.init() 133 | -------------------------------------------------------------------------------- /packages/millicast-publisher-demo/src/js/test/MillicastPublishUserMediaTest.js: -------------------------------------------------------------------------------- 1 | class MillicastPublishUserMediaTest { 2 | constructor() { 3 | this.mediaStream = null; 4 | this.options = { 5 | streamName: 'km0n0h1u', 6 | constraints: { 7 | audio: { 8 | echoCancellation: true, 9 | channelCount: { ideal: 2 }, 10 | }, 11 | video: { 12 | height: 1080, 13 | width: 1920 14 | }, 15 | } 16 | } 17 | this.millicastPublishUserMedia = null 18 | } 19 | 20 | async init() { 21 | this.millicastPublishUserMedia = await publisher.MillicastPublishUserMedia.build( 22 | this.options 23 | ); 24 | await this.setVideoSource(); 25 | await this.getDevices(); 26 | } 27 | 28 | async setVideoSource() { 29 | this.mediaStream = await this.millicastPublishUserMedia.getMediaStream(); 30 | console.log("GetMedia response:", this.mediaStream); 31 | document.getElementById( 32 | "millicast-media-video-test" 33 | ).srcObject = this.mediaStream; 34 | } 35 | 36 | async getDevices() { 37 | const audioInputSelect = document.getElementById("audio-input-select"); 38 | const audioOutputSelect = document.getElementById("audio-output-select"); 39 | const videoSelect = document.getElementById("video-select"); 40 | const devices = await this.millicastPublishUserMedia.devices; 41 | console.log("GetDevices response:", devices); 42 | this.fillSelectElement(audioInputSelect, devices.audioinput); 43 | this.fillSelectElement(audioOutputSelect, devices.audiooutput); 44 | this.fillSelectElement(videoSelect, devices.videoinput); 45 | } 46 | 47 | fillSelectElement(selectElement, devices) { 48 | selectElement.innerHTML = ""; 49 | for (const device of devices) 50 | selectElement.add(new Option(device.label, device.deviceId)); 51 | } 52 | 53 | async testStart(options = undefined) { 54 | const accountId = "tnJhvK"; 55 | const bandwidth = Number.parseInt( 56 | document.getElementById("bitrate-select").value 57 | ); 58 | 59 | const broadcastOptions = options ?? { 60 | bandwidth: bandwidth, 61 | disableVideo: false, 62 | disableAudio: false, 63 | }; 64 | try { 65 | const response = await this.millicastPublishUserMedia.connect( 66 | broadcastOptions 67 | ); 68 | console.log("BROADCASTING!! Start response: ", response); 69 | const viewLink = `https://viewer.millicast.com/v2?streamId=${accountId}/${broadcastOptions.streamName}`; 70 | console.log("Broadcast viewer link: ", viewLink); 71 | document.getElementById( 72 | "broadcast-status-label" 73 | ).innerHTML = `LIVE! View link: ${viewLink}`; 74 | } catch (error) { 75 | console.log("There was an error while trying to broadcast: ", error); 76 | } 77 | } 78 | 79 | testStop() { 80 | this.millicastPublishUserMedia.stop(); 81 | console.log("Broadcast stopped"); 82 | document.getElementById("broadcast-status-label").innerHTML = "READY!"; 83 | } 84 | 85 | async testUpdateBitrate(selectObject) { 86 | if (this.millicastPublishUserMedia.isActive()) { 87 | const bitrate = selectObject.value; 88 | await this.millicastPublishUserMedia.webRTCPeer.updateBitrate(bitrate); 89 | console.log("Bitrate updated"); 90 | } 91 | } 92 | 93 | testMuteAudio(checkboxObject) { 94 | const muted = this.millicastPublishUserMedia.muteMedia( 95 | "audio", 96 | checkboxObject.checked 97 | ); 98 | console.log("MuteMedia audio response:", muted); 99 | } 100 | 101 | testMuteVideo(checkboxObject) { 102 | const muted = this.millicastPublishUserMedia.muteMedia( 103 | "video", 104 | checkboxObject.checked 105 | ); 106 | console.log("MuteMedia video response:", muted); 107 | } 108 | 109 | async testChangeAudio(selectObject) { 110 | const deviceId = selectObject.value; 111 | this.mediaStream = await this.millicastPublishUserMedia.updateMediaStream( 112 | "audio", 113 | deviceId 114 | ); 115 | console.log("ChangeAudio response:", this.mediaStream); 116 | document.getElementById( 117 | "millicast-media-video-test" 118 | ).srcObject = this.mediaStream; 119 | return this.mediaStream; 120 | } 121 | 122 | async testChangeVideo(selectObject) { 123 | const deviceId = selectObject.value; 124 | this.mediaStream = await this.millicastPublishUserMedia.updateMediaStream( 125 | "video", 126 | deviceId 127 | ); 128 | console.log("ChangeVideo response:", this.mediaStream); 129 | document.getElementById( 130 | "millicast-media-video-test" 131 | ).srcObject = this.mediaStream; 132 | return this.mediaStream; 133 | } 134 | } 135 | 136 | const millicastPublishUserMediaTest = new MillicastPublishUserMediaTest(); 137 | millicastPublishUserMediaTest.init(); 138 | -------------------------------------------------------------------------------- /packages/millicast-sdk/tests/e2e/utils/Media.js: -------------------------------------------------------------------------------- 1 | class Media { 2 | constructor (options) { 3 | this.mediaStream = null 4 | 5 | this.constraints = { 6 | audio: { 7 | echoCancellation: false, 8 | channelCount: { ideal: 2 } 9 | }, 10 | video: true 11 | } 12 | /* Apply Options */ 13 | if (options && !!options.constraints) { Object.assign(this.constraints, options.constraints) } 14 | } 15 | 16 | /** 17 | * Get Enumerate Devices. 18 | * @example const devices = await millicastMedia.getDevices; 19 | * @returns {Promise} devices - sorted object containing arrays with audio devices and video devices. 20 | */ 21 | 22 | get getDevices () { 23 | return this.getMediaDevices() 24 | } 25 | 26 | getInput (kind) { 27 | let input = null 28 | if (!kind) return input 29 | if (this.mediaStream) { 30 | for (const track of this.mediaStream.getTracks()) { 31 | if (track.kind === kind) { 32 | input = track 33 | break 34 | } 35 | } 36 | } 37 | return input 38 | } 39 | 40 | /** 41 | * Get active video device. 42 | * @example const videoInput = millicastMedia.videoInput; 43 | * @returns {MediaStreamTrack} 44 | */ 45 | 46 | get videoInput () { 47 | return this.getInput('video') 48 | } 49 | 50 | /** 51 | * Get active audio device. 52 | * @example const audioInput = millicastMedia.audioInput; 53 | * @returns {MediaStreamTrack} 54 | */ 55 | 56 | get audioInput () { 57 | return this.getInput('audio') 58 | } 59 | 60 | /** 61 | * Get User Media. 62 | * @example const media = await MillicastMedia.getMedia(); 63 | * @returns {MediaStream} 64 | */ 65 | 66 | async getMedia () { 67 | // gets user cam and mic 68 | try { 69 | this.mediaStream = await navigator.mediaDevices.getUserMedia( 70 | this.constraints 71 | ) 72 | return this.mediaStream 73 | } catch (error) { 74 | console.error('Could not get Media: ', error, this.constraints) 75 | throw error 76 | } 77 | } 78 | 79 | async getMediaDevices () { 80 | if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { 81 | throw new Error( 82 | 'Could not get list of media devices! This might not be supported by this browser.' 83 | ) 84 | } 85 | 86 | try { 87 | const items = { audioinput: [], videoinput: [], audiooutput: [] } 88 | const mediaDevices = await navigator.mediaDevices.enumerateDevices() 89 | for (const device of mediaDevices) { this.addMediaDevicesToList(items, device) } 90 | this.devices = items 91 | } catch (error) { 92 | console.error('Could not get Media: ', error) 93 | this.devices = [] 94 | } 95 | return this.devices 96 | } 97 | 98 | addMediaDevicesToList (items, device) { 99 | if (device.deviceId !== 'default' && items[device.kind]) { items[device.kind].push(device) } 100 | } 101 | 102 | /** 103 | * @param {String} id - the id from the selected video device. 104 | * @example const media = await millicastMedia.changeVideo(id); 105 | * @returns {MediaStream} - stream from the latest selected video device. 106 | */ 107 | 108 | async changeVideo (id) { 109 | return await this.changeSource(id, 'video') 110 | } 111 | 112 | /** 113 | * @param {String} id - the id from the selected audio device. 114 | * @example const media = await millicastMedia.changeAudio(id); 115 | * @returns {MediaStream} - stream from the latest selected audio device. 116 | */ 117 | 118 | async changeAudio (id) { 119 | return await this.changeSource(id, 'audio') 120 | } 121 | 122 | async changeSource (id, sourceType) { 123 | if (!id) throw new Error('Required id') 124 | 125 | this.constraints[sourceType] = { 126 | ...this.constraints[sourceType], 127 | deviceId: { 128 | exact: id 129 | } 130 | } 131 | return await this.getMedia() 132 | } 133 | 134 | /** 135 | * @param {boolean} boolean - true if you want to mute the video, false for mute it. 136 | * @returns {boolean} - returns true if it was changed, otherwise returns false. 137 | */ 138 | 139 | muteVideo (boolean = true) { 140 | let changed = false 141 | if (this.mediaStream) { 142 | this.mediaStream.getVideoTracks()[0].enabled = !boolean 143 | changed = true 144 | } else { 145 | console.error('There is no media stream object.') 146 | } 147 | return changed 148 | } 149 | 150 | /** 151 | * @param {boolean} boolean - true if you want to mute the audio, false for mute it. 152 | * @returns {boolean} - returns true if it was changed, otherwise returns false. 153 | */ 154 | 155 | muteAudio (boolean = true) { 156 | let changed = false 157 | if (this.mediaStream) { 158 | this.mediaStream.getAudioTracks()[0].enabled = !boolean 159 | changed = true 160 | } else { 161 | console.error('There is no media stream object.') 162 | } 163 | return changed 164 | } 165 | } 166 | 167 | window.media = new Media({ audio: true, video: true }) 168 | --------------------------------------------------------------------------------