diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8efdb67 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "editor.formatOnSaveMode": "file", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 401bde5..454b81e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,46 +1,65 @@ # Changelog + All changes to this project will be documented in this file. +## [1.2.29] - 2024-06-19 + +- Add custom domain + ## [1.2.28] - 2023-12-11 + - Add experimental methods ## [1.2.27] - 2023-12-08 + - Add chromecast & airplay events ## [1.2.26] - 2023-10-05 + - Add isLiveStream() ## [1.2.25] - 2023-06-02 + - Add ads support ## [1.2.24] - 2023-03-03 + - Add `sequence` option - + ## [1.2.23] - 2023-02-15 + - Add `privateSession` option ## [1.2.22] - 2022-11-04 + - Add `getPlaying()` method ## [1.2.21] - 2022-05-23 + - Apply `setVideoStyleObjectFit()` and `setVideoStyleTransform()` to poster. ## [1.2.20] - 2022-05-23 + - Add `setVideoStyleObjectFit()` and `setVideoStyleTransform()` methods. - + ## [1.2.19] - 2022-05-19 + - When loadConfig() is called, apply the last config settings - + ## [1.2.18] - 2022-05-12 + - Fix showControls & hideControls methods when called before the player is ready. - + ## [1.2.17] - 2022-04-27 + - add exitFullscreen(), requestFullscreen(), exitPictureInPicture() and requestPictureInPicture() ## [1.2.16] - 2022-04-25 + - add showPoster() and hidePoster() ## [1.2.15] - 2022-04-20 + - Add event callback types - Delay some methods execution after ready event is received - Add playbackRate option @@ -48,47 +67,61 @@ All changes to this project will be documented in this file. - Add setAutoplay() method ## [1.2.14] - 2022-04-11 + - Add `chromeless` mode - Add `hidePoster` option ## [1.2.13] - 2022-04-01 + - Add the possibility to show/hide the "more" button ## [1.2.12] - 2021-12-23 + - Add the possibility to directly provide a DOM Element at instantiation - + ## [1.2.11] - 2021-12-15 + - Add `controls` parameter in `hideControls()` and `showControls()` ## [1.2.10] - 2021-12-10 + - Add `allow="autoplay"` in iframe tag ## [1.2.9] - 2021-10-07 + - Add setTheme method ## [1.2.8] - 2021-05-28 + - Add showSubtitles() / hideSubtitles() methods - + ## [1.2.7] - 2021-05-28 + - Add "token" in SdkOptions type ## [1.2.6] - 2021-04-21 + - Add setPlaybackRate(), showControls(), hideControls() & getPlaybackRate methods -- Add new player events +- Add new player events ## [1.2.5] - 2021-03-16 + - Add loadConfig method - Add "hideTitle" & "token" options ## [1.2.4] - 2021-02-25 + - Add "hideControls" and "loop" options ## [1.2.3] - 2021-02-02 + - Fix a string/number comparaison ## [1.2.2] - 2021-02-02 + - getPaused, getMuted, getDuration, getCurrentTime, getVolume and getLoop now return promises - Add some unit test ## [1.2.1] - 2021-01-15 -- Add the possibility to specify metadata \ No newline at end of file + +- Add the possibility to specify metadata diff --git a/README.md b/README.md index 07a7007..7509aad 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ + [![badge](https://img.shields.io/twitter/follow/api_video?style=social)](https://twitter.com/intent/follow?screen_name=api_video)   [![badge](https://img.shields.io/github/stars/apivideo/api.video-player-sdk?style=social)](https://github.com/apivideo/api.video-player-sdk)   [![badge](https://img.shields.io/discourse/topics?server=https%3A%2F%2Fcommunity.api.video)](https://community.api.video) ![](https://github.com/apivideo/.github/blob/main/assets/apivideo_banner.png) +

api.video player SDK

[api.video](https://api.video) is the video infrastructure for product builders. Lightning fast video APIs for integrating, scaling, and managing on-demand & low latency live streaming features in your app. - ## Table of contents - [Table of contents](#table-of-contents) @@ -21,15 +22,15 @@ - [Methods](#methods) - [Full example](#full-example) - [Control an existing embedded player using the SDK](#control-an-existing-embedded-player-using-the-sdk) - - - + ## Project description SDK to control and interact with the api.video HTML5 Player - ## Getting started ### Installation #### Method #1: requirejs -If you use requirejs you can add the SDK as a dependency to your project with +If you use requirejs you can add the SDK as a dependency to your project with ```sh $ npm install --save @api.video/player-sdk ``` -You can then use the SDK in your script: +You can then use the SDK in your script: ```javascript -var { PlayerSdk } = require('@api.video/player-sdk'); +var { PlayerSdk } = require("@api.video/player-sdk"); var sdk = new PlayerSdk("#target", { - id: "", - // ... other optional options -}); + id: "", + // ... other optional options +}); ``` #### Method #2: typescript -If you use Typescript you can add the SDK as a dependency to your project with +If you use Typescript you can add the SDK as a dependency to your project with ```sh $ npm install --save @api.video/player-sdk ``` -You can then use the SDK in your script: +You can then use the SDK in your script: ```typescript -import { PlayerSdk } from '@api.video/player-sdk' +import { PlayerSdk } from "@api.video/player-sdk"; const sdk = new PlayerSdk("#target", { - id: "", - // ... other optional options + id: "", + // ... other optional options }); ``` - #### Method #2: simple include in a javascript project Include the SDK in your HTML file like so: ```html - ... - + ... + ``` Then, once the `window.onload` event has been triggered, create your player using `new PlayerSdk()`: + ```html ``` ## Documentation ### Instantiation - + The PlayerSdk constructor takes 2 parameters: + - `targetSelector: string | Element` a CSS selector targeting the DOM element in which you want to create the player (eg. "#target"), or the DOM element itself - `options: SdkOptions` an object containing the player options. The available options are the following: - -| Option name | Mandatory | Type | Description | -| -------------: | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------ | -| id | **yes** | string | the id of the video (videoId or liveStreamId) | -| token | yes for private video | string | the [private video](https://api.video/blog/tutorials/tutorial-private-videos/) url token | -| privateSession | no | string | the [private video](https://api.video/blog/tutorials/tutorial-private-videos/) session id if needed | -| live | no (default: false) | boolean | indicate that the video is a live one | -| autoplay | no (default: false) | boolean | start playing the video as soon as it is loaded | -| muted | no (default: false) | boolean | the video is muted | -| metadata | no (default: empty) | object | object containing [metadata](https://api.video/blog/tutorials/dynamic-metadata/) (see **Full example** below) | -| hideControls | no (default: false) | boolean | the controls are hidden (except unmute button if the video starts muted) | -| chromeless | no (default: false) | boolean | chromeless mode: all controls are hidden | -| hideTitle | no (default: false) | boolean | the video title is hidden | -| hidePoster | no (default: false) | boolean | the poster image isn't displayed | -| showSubtitles | no (default: false) | boolean | the video subtitles are shown by default | -| loop | no (default: false) | boolean | once the video is finished it automatically starts again | -| playbackRate | no (default: 1) | number | the playback rate of the video: 1 for normal, 2 for x2, etc. | -| sequence | no | {start: number, end: number} | define a sequence of the video to play. The video will start at the `start` timecode and end at the `end` timecode. The timecodes are in seconds. | -| ads | no | {adTagUrl: string} | see below [ads](#ads) | - +| Option name | Mandatory | Type | Description | +| -------------: | --------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | **yes** | string | the id of the video (videoId or liveStreamId) | +| token | yes for private video | string | the [private video](https://api.video/blog/tutorials/tutorial-private-videos/) url token | +| privateSession | no | string | the [private video](https://api.video/blog/tutorials/tutorial-private-videos/) session id if needed | +| live | no (default: false) | boolean | indicate that the video is a live one | +| autoplay | no (default: false) | boolean | start playing the video as soon as it is loaded | +| muted | no (default: false) | boolean | the video is muted | +| metadata | no (default: empty) | object | object containing [metadata](https://api.video/blog/tutorials/dynamic-metadata/) (see **Full example** below) | +| hideControls | no (default: false) | boolean | the controls are hidden (except unmute button if the video starts muted) | +| chromeless | no (default: false) | boolean | chromeless mode: all controls are hidden | +| hideTitle | no (default: false) | boolean | the video title is hidden | +| hidePoster | no (default: false) | boolean | the poster image isn't displayed | +| showSubtitles | no (default: false) | boolean | the video subtitles are shown by default | +| loop | no (default: false) | boolean | once the video is finished it automatically starts again | +| playbackRate | no (default: 1) | number | the playback rate of the video: 1 for normal, 2 for x2, etc. | +| sequence | no | {start: number, end: number} | define a sequence of the video to play. The video will start at the `start` timecode and end at the `end` timecode. The timecodes are in seconds. | +| ads | no | {adTagUrl: string} | see below [ads](#ads) | +| customDomain | no | string | if you've enabled Custom Domains for your account, the complete 'embed' domain (eg. embed.mydomain.com) | The sdk instance can be used to control the video playback, and to listen to player events. #### Ads + Ads can be displayed in the player. To do so, you need to pass the `ads` option to the sdk constructor. In the `ads` object, pass the `adTagUrl` property with the url of the ad tag. The ad tag must be a VAST 2.0 or 3.0 url. For more information about VAST, check the [IAB documentation](https://www.iab.com/guidelines/vast/). Note: ads are displayed using the [Google IMA SDK](https://developers.google.com/interactive-media-ads/docs/sdks/html5/quickstart). @@ -151,13 +153,15 @@ The sdk instance has the following methods: Load a new video in the same instance of the player. Available options are the same as the ones passed to the SDK constructor (see available). > Example: + ```javascript - player.loadConfig({ - id: "", - hideTitle: true, - hideControls: true, - }); -``` +player.loadConfig({ + id: "", + hideTitle: true, + hideControls: true, +}); +``` + **`play()`** Start playing the video. @@ -178,54 +182,86 @@ Unmute the video. Hide the player controls. - `controls` parameter type definition: -```typescript -type ControlName = "play" | "seekBackward" | "seekForward" | "playbackRate" - | "volume" | "fullscreen" | "subtitles" | "chapters" - | "pictureInPicture" | "progressBar" | "chromecast" | "download" | "more"; + +```typescript +type ControlName = + | "play" + | "seekBackward" + | "seekForward" + | "playbackRate" + | "volume" + | "fullscreen" + | "subtitles" + | "chapters" + | "pictureInPicture" + | "progressBar" + | "chromecast" + | "download" + | "more"; ``` + > If no value is provided for the "controls" parameter, all controls will be hidden. **Note**: the only control that can still be visible is the unmute button if the video as started muted. To hide all controls, including this one, use the setChromeless() method Example: + ```javascript - player.hideControls(); -``` +player.hideControls(); +``` + > If a list of control names if provided, the associated controls will be hidden. Example: + ```javascript - player.showControls(); // display all controls ... - player.hideControls(["download", "subtitles"]); // ... except "download" and "subtitles" -``` +player.showControls(); // display all controls ... +player.hideControls(["download", "subtitles"]); // ... except "download" and "subtitles" +``` + **`showControls(controls?: ControlName[])`** Show the player controls. - `controls` parameter type definition: -```typescript -type ControlName = "play" | "seekBackward" | "seekForward" | "playbackRate" - | "volume" | "fullscreen" | "subtitles" | "chapters" - | "pictureInPicture" | "progressBar" | "chromecast" | "download" | "more"; + +```typescript +type ControlName = + | "play" + | "seekBackward" + | "seekForward" + | "playbackRate" + | "volume" + | "fullscreen" + | "subtitles" + | "chapters" + | "pictureInPicture" + | "progressBar" + | "chromecast" + | "download" + | "more"; ``` + > If no value is provided for the "controls" parameter, all controls will be displayed. Example: + ```javascript - player.showControls(); -``` +player.showControls(); +``` + > If a list of control names if provided, the associated controls will be displayed. Example: + ```javascript - player.hideControls(); // hide all controls ... - player.showControls(["download", "subtitles"]); // ... except "download" and "subtitles" ... - // ... - player.showControls(["progressBar"]); // ... and the progress bar -``` +player.hideControls(); // hide all controls ... +player.showControls(["download", "subtitles"]); // ... except "download" and "subtitles" ... +// ... +player.showControls(["progressBar"]); // ... and the progress bar +``` + **`setChromeless(chromeless: boolean)`** Define if the player should be in chromeless mode (all controls hidden). @@ -252,84 +288,92 @@ Define if the video should be played in loop. **`setAutoplay(autoplay: boolean)`** -Define if the video should start playing as soon as it is loaded +Define if the video should start playing as soon as it is loaded **`seek(time: number)`** -Add/substract the given number of seconds to/from the playback time. +Add/substract the given number of seconds to/from the playback time. **`setPlaybackRate(rate: number)`** -Set the current playback rate. +Set the current playback rate. Example: + ```javascript - player.setPlaybackRate(2); // Play at 2x rate +player.setPlaybackRate(2); // Play at 2x rate ``` + **`setCurrentTime(time: number)`** -Set the current playback time (seconds). +Set the current playback time (seconds). + +> Example: ->Example: ```javascript - player.setCurrentTime(24); // Go the 24th second -``` +player.setCurrentTime(24); // Go the 24th second +``` + **`setVolume(volume: number)`** Change the audio volume to the given value. From 0 to 1 (0 = muted, 1 = 100%). > Example: + ```javascript - player.setVolume(0.75); // Set the volume to 75% -``` +player.setVolume(0.75); // Set the volume to 75% +``` + **`setVideoStyleObjectFit(value: "contain" | "cover" | "fill" | "none" | "scale-down")`** Change the [object-fit](https://developer.mozilla.org/fr/docs/Web/CSS/object-fit) CSS value of the video tag. - Example: + ```javascript - player.setVideoStyleObjectFit("cover"); // Set the object-fit to cover -``` +player.setVideoStyleObjectFit("cover"); // Set the object-fit to cover +``` **`setVideoStyleTransform(value: string)`** Change the [transform](https://developer.mozilla.org/fr/docs/Web/CSS/transform) CSS value of the video tag. - Example: + ```javascript - player.setVideoStyleTransform("rotateY(180deg)"); // Apply a 180deg rotation around the Y axis (mirroring) -``` +player.setVideoStyleTransform("rotateY(180deg)"); // Apply a 180deg rotation around the Y axis (mirroring) +``` **`setTheme(theme: PlayerTheme)`** Change the appearance of the player. - `theme` parameter type definition: + ```typescript type PlayerTheme = { - text?: string; - link?: string; - linkHover?: string; - trackPlayed?: string; - trackUnplayed?: string; - trackBackground?: string; - backgroundTop?: string; - backgroundBottom?: string; - backgroundText?: string; - linkActive?: string; -} + text?: string; + link?: string; + linkHover?: string; + trackPlayed?: string; + trackUnplayed?: string; + trackBackground?: string; + backgroundTop?: string; + backgroundBottom?: string; + backgroundText?: string; + linkActive?: string; +}; ``` + > Example: + ```javascript - player.setTheme({ - link: "red", - linkHover: "rgba(0, 255, 0, 1)", - backgroundBottom: "#0000ff", - }); -``` +player.setTheme({ + link: "red", + linkHover: "rgba(0, 255, 0, 1)", + backgroundBottom: "#0000ff", +}); +``` **`requestFullscreen()`** @@ -337,7 +381,7 @@ Request fullscreen mode (this may not work in some cases depending on browser re **`exitFullscreen()`** -Leave fullscreen mode +Leave fullscreen mode **`requestPictureInPicture()`** @@ -345,7 +389,7 @@ Request picture in picture mode (this may not work in some cases depending on br **`exitPictureInPicture()`** -Leave picture in picture mode +Leave picture in picture mode **`getPaused(callback?: (paused: boolean) => void): Promise`** @@ -389,92 +433,96 @@ Destroy the player instance. **`addEventListener(event: string, callback: () => void)`** -Define a callback function that will be called when the given event is triggered by the player. - +Define a callback function that will be called when the given event is triggered by the player. Available events are the following: -Event name | Description | Parameter ----: | --- | --- -airplayConnected | Started to play on an airplay device | - -airplayDisconnected | Stopped to play on an airplay device | - -chromecastConnected | Started to play on a chromecast device | - -chromecastDisconnected | Stopped to play on a chromecast device | - -controlsdisabled | Controls are now disabled | - -controlsenabled | Controls are now enabled | - -ended | The playback as reached the ended of the video | - -error | An error occured | - -firstplay | The video started to play for the first time | - -fullscreenchange | The player goes to (or goes back from) full screen | - -mouseenter | The user's mouse entered the player area | - -mouseleave | The user's mouse leaved the player area | - -pause | The video has been paused | - -play | The video started to play (for the first time or after having been paused) | - -playerresize | The player size has changed | - -qualitychange | The video quality has changed | `{ resolution: { height: number, width: number } }` -ratechange | The playback rate has changed | - -ready | The player is ready to play | - -resize | The video size has changed -seeking | The player is seeking | - -timeupdate | The playback time has changed | `{ currentTime: number }` -useractive | The user is active | - -userinactive | The user is inactive | - -volumechange | The volume has changed | `{ volume: number }` +| Event name | Description | Parameter | +| ---------------------: | -------------------------------------------------------------------------- | --------------------------------------------------- | +| airplayConnected | Started to play on an airplay device | - | +| airplayDisconnected | Stopped to play on an airplay device | - | +| chromecastConnected | Started to play on a chromecast device | - | +| chromecastDisconnected | Stopped to play on a chromecast device | - | +| controlsdisabled | Controls are now disabled | - | +| controlsenabled | Controls are now enabled | - | +| ended | The playback as reached the ended of the video | - | +| error | An error occured | - | +| firstplay | The video started to play for the first time | - | +| fullscreenchange | The player goes to (or goes back from) full screen | - | +| mouseenter | The user's mouse entered the player area | - | +| mouseleave | The user's mouse leaved the player area | - | +| pause | The video has been paused | - | +| play | The video started to play (for the first time or after having been paused) | - | +| playerresize | The player size has changed | - | +| qualitychange | The video quality has changed | `{ resolution: { height: number, width: number } }` | +| ratechange | The playback rate has changed | - | +| ready | The player is ready to play | - | +| resize | The video size has changed | +| seeking | The player is seeking | - | +| timeupdate | The playback time has changed | `{ currentTime: number }` | +| useractive | The user is active | - | +| userinactive | The user is inactive | - | +| volumechange | The volume has changed | `{ volume: number }` | Examples: + ```javascript - // listen to the 'play' event - player.addEventListener('play', function() { - console.log('play event received'); - }); +// listen to the 'play' event +player.addEventListener("play", function () { + console.log("play event received"); +}); - player.addEventListener('qualitychange', function(ev) { - console.log(`quality has changed: ${ev.resolution.width}x${ev.resolution.height}`); - }); +player.addEventListener("qualitychange", function (ev) { + console.log( + `quality has changed: ${ev.resolution.width}x${ev.resolution.height}` + ); +}); ``` ### Full example ```html - - ... - - - - -
- - - - - - - - - + + ... + + + + +
+ + + + + + + + + ``` @@ -484,21 +532,27 @@ It's also possible to integrate the SDK in a page that already contains an embed ```html - - ... - - - - ... - - - ... - + + ... + + + + ... + + + ... + ``` To attach the SDK to this player, you'll have to make the following changed in your page: - + - import the `sdk.js` script in your page, - create a `PlayerSdk` instance once the page is loaded. @@ -506,25 +560,32 @@ Here is how the page will look like with these changes : ```html - - ... - - - - - ... - - - ... - - - + + ... + + + + + ... + + + ... + + + ``` diff --git a/index.ts b/index.ts index 603998e..a45e5d5 100644 --- a/index.ts +++ b/index.ts @@ -1,527 +1,660 @@ type UserEventListener = { - event: string; - callback: (data?: any) => void; -} + event: string; + callback: (data?: any) => void; +}; type PlayerEvent = { - type: string; - data: any; -} + type: string; + data: any; +}; export type PlayerTheme = { - text?: string; - link?: string; - linkHover?: string; - trackPlayed?: string; - trackUnplayed?: string; - trackBackground?: string; - backgroundTop?: string; - backgroundBottom?: string; - backgroundText?: string; - linkActive?: string; -} + text?: string; + link?: string; + linkHover?: string; + trackPlayed?: string; + trackUnplayed?: string; + trackBackground?: string; + backgroundTop?: string; + backgroundBottom?: string; + backgroundText?: string; + linkActive?: string; +}; type AdsOptions = { - adTagUrl: string; -} + adTagUrl: string; +}; type SdkOptions = { - id: string; - live?: boolean; - autoplay?: boolean; - muted?: boolean; - metadata?: { - [key: string]: string; - } - hideControls?: boolean; - hidePoster?: boolean; - chromeless?: boolean; - loop?: boolean; - hideTitle?: boolean; - iframeUrl?: string; - token?: string; - privateSession?: string; - showSubtitles?: boolean; - ads?: AdsOptions; - playbackRate?: number; - sequence?: { - start: number; - end: number; - } - // This feature is experimental. - // It may change or be removed at any time and could cause significant playback issues. - minimalQuality?: number; - // This feature is experimental. - // It may change or be removed at any time and could cause significant playback issues. - maximalQuality?: number; -} + id: string; + live?: boolean; + autoplay?: boolean; + muted?: boolean; + metadata?: { + [key: string]: string; + }; + hideControls?: boolean; + hidePoster?: boolean; + chromeless?: boolean; + loop?: boolean; + hideTitle?: boolean; + iframeUrl?: string; + token?: string; + privateSession?: string; + showSubtitles?: boolean; + ads?: AdsOptions; + playbackRate?: number; + sequence?: { + start: number; + end: number; + }; + customDomain?: string; + // This feature is experimental. + // It may change or be removed at any time and could cause significant playback issues. + minimalQuality?: number; + // This feature is experimental. + // It may change or be removed at any time and could cause significant playback issues. + maximalQuality?: number; +}; type PlayerSdkEvent = { - airplayConnected: () => void; - airplayDisconnected: () => void; - chromecastConnected: () => void; - chromecastDisconnected: () => void; - controlsdisabled: () => void; - controlsenabled: () => void; - ended: () => void; - error: () => void; - firstplay: () => void; - fullscreenchange: () => void; - mouseenter: () => void; - mouseleave: () => void; - pause: () => void; - play: () => void; - playerresize: () => void; - qualitychange: (value: { resolution: { height: number, width: number }}) => void; - ratechange: () => void; - ready: () => void; - resize: () => void; - seeking: () => void; - timeupdate: (value: { currentTime: number}) => void; - useractive: () => void; - userinactive: () => void; - volumechange: (value: { volume: number}) => void; -} - -export type ControlName = "play" | "seekBackward" | "seekForward" | "playbackRate" - | "volume" | "fullscreen" | "subtitles" | "chapters" - | "pictureInPicture" | "progressBar" | "chromecast" | "download" | "more"; - + airplayConnected: () => void; + airplayDisconnected: () => void; + chromecastConnected: () => void; + chromecastDisconnected: () => void; + controlsdisabled: () => void; + controlsenabled: () => void; + ended: () => void; + error: () => void; + firstplay: () => void; + fullscreenchange: () => void; + mouseenter: () => void; + mouseleave: () => void; + pause: () => void; + play: () => void; + playerresize: () => void; + qualitychange: (value: { + resolution: { height: number; width: number }; + }) => void; + ratechange: () => void; + ready: () => void; + resize: () => void; + seeking: () => void; + timeupdate: (value: { currentTime: number }) => void; + useractive: () => void; + userinactive: () => void; + volumechange: (value: { volume: number }) => void; +}; + +export type ControlName = + | "play" + | "seekBackward" + | "seekForward" + | "playbackRate" + | "volume" + | "fullscreen" + | "subtitles" + | "chapters" + | "pictureInPicture" + | "progressBar" + | "chromecast" + | "download" + | "more"; export class PlayerSdk { - private static DEFAULT_IFRAME_URL = "https://embed.api.video/${type}/${id}"; - - private iframe: HTMLIFrameElement | null = null; - private sdkInSync: boolean = false; - private currentVideoReady: boolean = false; - private onceSdkInSyncCallbacks: (() => void)[] = []; - private onceVideoReadyCallbacks: (() => void)[] = []; - private userEventListeners: UserEventListener[] = []; - private sdkPlayerId: number; - private sdkOrigin: string; - private playerOrigin: string | null = null; - private postMessageCallbacks: { [callbackId: string]: (arg: any) => void } = {}; - private iframeUrl: string; - private options: SdkOptions; - - static nextSdkPlayerId: number = 1; - - constructor(targetSelector: string | Element, userOptions?: SdkOptions) { - this.sdkPlayerId = PlayerSdk.nextSdkPlayerId++; - - this.sdkOrigin = `${window.location.protocol}//${window.location.host}`; - - const target = targetSelector instanceof Element - ? targetSelector - : document.querySelector(targetSelector); - - if (target == null) { - throw new Error("No match found for selector " + targetSelector); - } - - this.iframe = target.tagName !== "IFRAME" - ? this.createIframe(target) - : target as HTMLIFrameElement - - const options = userOptions || {} as SdkOptions; - this.iframeUrl = options.iframeUrl || PlayerSdk.DEFAULT_IFRAME_URL; - - if (!this.iframe.src) { - this.createNewPlayer(this.iframe, options) - } else { - this.bindExistingPlayer(this.iframe); - } - - this.onceSdkInSyncCallbacks = []; - this.userEventListeners = []; - this.sdkInSync = false; - this.currentVideoReady = false; - this.playerOrigin = new URL(this.iframeUrl).origin; - this.options = options; - - window.addEventListener("message", (message) => { - if (message.origin === this.playerOrigin && parseInt(message.data?.sdkPlayerId, 10) === this.sdkPlayerId) { - if (!!message.data.callbackId && !!this.postMessageCallbacks[message.data.callbackId]) { - this.postMessageCallbacks[message.data.callbackId](message.data.arg); - } else { - this.onEvent(message.data); - } - } - }, false); - - if(options.playbackRate) { - this.setPlaybackRate(options.playbackRate); + private iframe: HTMLIFrameElement | null = null; + private sdkInSync: boolean = false; + private currentVideoReady: boolean = false; + private onceSdkInSyncCallbacks: (() => void)[] = []; + private onceVideoReadyCallbacks: (() => void)[] = []; + private userEventListeners: UserEventListener[] = []; + private sdkPlayerId: number; + private sdkOrigin: string; + private playerOrigin: string | null = null; + private postMessageCallbacks: { [callbackId: string]: (arg: any) => void } = + {}; + private iframeUrl: string; + private options: SdkOptions; + + static nextSdkPlayerId: number = 1; + + constructor(targetSelector: string | Element, userOptions?: SdkOptions) { + this.sdkPlayerId = PlayerSdk.nextSdkPlayerId++; + + this.sdkOrigin = `${window.location.protocol}//${window.location.host}`; + + const target = + targetSelector instanceof Element + ? targetSelector + : document.querySelector(targetSelector); + + if (target == null) { + throw new Error("No match found for selector " + targetSelector); + } + + this.iframe = + target.tagName !== "IFRAME" + ? this.createIframe(target) + : (target as HTMLIFrameElement); + + const options = userOptions || ({} as SdkOptions); + this.iframeUrl = this.getIframeUrl(options); + + if (!this.iframe.src) { + this.createNewPlayer(this.iframe, options); + } else { + this.bindExistingPlayer(this.iframe); + } + + this.onceSdkInSyncCallbacks = []; + this.userEventListeners = []; + this.sdkInSync = false; + this.currentVideoReady = false; + this.playerOrigin = new URL(this.iframeUrl).origin; + this.options = options; + + window.addEventListener( + "message", + (message) => { + if ( + message.origin === this.playerOrigin && + parseInt(message.data?.sdkPlayerId, 10) === this.sdkPlayerId + ) { + if ( + !!message.data.callbackId && + !!this.postMessageCallbacks[message.data.callbackId] + ) { + this.postMessageCallbacks[message.data.callbackId]( + message.data.arg + ); + } else { + this.onEvent(message.data); + } } - } + }, + false + ); - loadConfig(options: SdkOptions) { - this.currentVideoReady = false; - this.options = { - ...this.options, - ...options, - }; - this.postMessage({ - message: 'loadConfig', - url: this.buildPlayerUrl(this.options) - }); + if (options.playbackRate) { + this.setPlaybackRate(options.playbackRate); } + } - play() { - this.postMessage({ message: 'play' }); + private getIframeUrl(options: SdkOptions) { + if (options.iframeUrl) { + return options.iframeUrl; } + const domain = options.customDomain || "embed.api.video"; + return "https://" + domain + "/${type}/${id}"; + } - setVideoStyleTransform(value: string) { - this.postMessage({ message: 'setStyleProperty', query: "video", property: "transform", value }, undefined, false); - this.postMessage({ message: 'setStyleProperty', query: ".vjs-poster", property: "transform", value }, undefined, false); - } - - setVideoStyleObjectFit(value: "contain" | "cover" | "fill" | "none" | "scale-down") { - this.postMessage({ message: 'setStyleProperty', query: "video", property: "object-fit", value }, undefined, false); - this.postMessage({ message: 'setStyleProperty', query: ".vjs-poster", property: "background-size", value }, undefined, false); - } - - // This feature is experimental. - // It may change or be removed at any time and could cause significant playback issues. - setMinimalQuality(quality: number) { - this.postMessage({ message: "setMinimalQuality", quality}) + loadConfig(options: SdkOptions) { + this.currentVideoReady = false; + this.options = { + ...this.options, + ...options, }; - - // This feature is experimental. - // It may change or be removed at any time and could cause significant playback issues. - setMaximalQuality(quality: number) { - this.postMessage({ message: "setMaximalQuality", quality}) + this.postMessage({ + message: "loadConfig", + url: this.buildPlayerUrl(this.options), + }); + } + + play() { + this.postMessage({ message: "play" }); + } + + setVideoStyleTransform(value: string) { + this.postMessage( + { + message: "setStyleProperty", + query: "video", + property: "transform", + value, + }, + undefined, + false + ); + this.postMessage( + { + message: "setStyleProperty", + query: ".vjs-poster", + property: "transform", + value, + }, + undefined, + false + ); + } + + setVideoStyleObjectFit( + value: "contain" | "cover" | "fill" | "none" | "scale-down" + ) { + this.postMessage( + { + message: "setStyleProperty", + query: "video", + property: "object-fit", + value, + }, + undefined, + false + ); + this.postMessage( + { + message: "setStyleProperty", + query: ".vjs-poster", + property: "background-size", + value: value !== "fill" ? value : "100% 100%", + }, + undefined, + false + ); + } + + // This feature is experimental. + // It may change or be removed at any time and could cause significant playback issues. + setMinimalQuality(quality: number) { + this.postMessage({ message: "setMinimalQuality", quality }); + } + + // This feature is experimental. + // It may change or be removed at any time and could cause significant playback issues. + setMaximalQuality(quality: number) { + this.postMessage({ message: "setMaximalQuality", quality }); + } + + hideControls(controls?: ControlName[]) { + if (!controls) { + this.options.hideControls = true; + } + this.postMessage({ message: "hideControls", controls }, undefined, true); + } + showControls(controls?: ControlName[]) { + if (!controls) { + this.options.hideControls = false; + } + this.postMessage({ message: "showControls", controls }, undefined, true); + } + hideSubtitles() { + this.options.showSubtitles = false; + this.postMessage({ message: "hideSubtitles" }); + } + showSubtitles() { + this.options.showSubtitles = true; + this.postMessage({ message: "showSubtitles" }); + } + hideTitle() { + this.options.hideTitle = true; + this.postMessage({ message: "hideTitle" }); + } + showTitle() { + this.options.hideTitle = false; + this.postMessage({ message: "showTitle" }); + } + hidePoster() { + this.options.hidePoster = true; + this.postMessage({ message: "hidePoster" }); + } + showPoster() { + this.options.hidePoster = false; + this.postMessage({ message: "showPoster" }); + } + pause() { + this.postMessage({ message: "pause" }); + } + mute() { + this.options.muted = true; + this.postMessage({ message: "mute" }); + } + unmute() { + this.options.muted = false; + this.postMessage({ message: "unmute" }); + } + seek(time: number) { + this.postMessage({ message: "seek", seek: time }); + } + setCurrentTime(time: number) { + this.postMessage({ message: "setCurrentTime", currentTime: time }); + } + setVolume(volume: number) { + this.postMessage({ message: "setVolume", volume }); + } + setAutoplay(autoplay: boolean) { + this.options.autoplay = autoplay; + this.postMessage({ message: "setAutoplay", autoplay }); + } + setLoop(loop: boolean) { + this.options.loop = loop; + this.postMessage({ message: "setLoop", loop }); + } + setChromeless(chromeless: boolean) { + this.options.chromeless = chromeless; + this.postMessage({ message: "setChromeless", chromeless }); + } + setPlaybackRate(rate: number) { + this.options.playbackRate = rate; + this.postMessage({ message: "setPlaybackRate", rate }, undefined, true); + } + exitFullscreen() { + this.postMessage({ message: "exitFullscreen" }, undefined, true); + } + requestFullscreen() { + this.postMessage({ message: "requestFullscreen" }, undefined, true); + } + exitPictureInPicture() { + this.postMessage({ message: "exitPictureInPicture" }, undefined, true); + } + requestPictureInPicture() { + this.postMessage({ message: "requestPictureInPicture" }, undefined, true); + } + setTheme(theme: PlayerTheme) { + this.postMessage({ message: "setTheme", theme }); + } + getPaused(callback?: (paused: boolean) => void): Promise { + return this.postMessage({ message: "getPaused" }, callback); + } + getMuted(callback?: (muted: boolean) => void): Promise { + return this.postMessage({ message: "getMuted" }, callback); + } + getPlaying(callback?: (muted: boolean) => void): Promise { + return this.postMessage({ message: "getPlaying" }, callback); + } + getDuration(callback?: (duration: number) => void): Promise { + return this.postMessage({ message: "getDuration" }, callback, true); + } + getCurrentTime(callback?: (currentTime: number) => void): Promise { + return this.postMessage({ message: "getCurrentTime" }, callback); + } + getPlaybackRate(callback?: (rate: number) => void): Promise { + return this.postMessage({ message: "getPlaybackRate" }, callback); + } + getVolume(callback?: (volume: number) => void): Promise { + return this.postMessage({ message: "getVolume" }, callback); + } + getLoop(callback?: (loop: boolean) => void): Promise { + return this.postMessage({ message: "getLoop" }, callback); + } + getVideoSize( + callback?: (size: { width: number; height: number }) => void + ): Promise<{ width: number; height: number }> { + return this.postMessage({ message: "getVideoSize" }, callback, true); + } + isLiveStream(callback?: (isLiveStream: boolean) => void): Promise { + return this.postMessage({ message: "isLiveStream" }, callback); + } + + addEventListener( + event: K, + callback: PlayerSdkEvent[K] + ) { + this.userEventListeners.push({ event, callback }); + } + + destroy() { + this.postMessage({ message: "destroy" }); + setTimeout(() => this.iframe?.parentElement?.removeChild(this.iframe), 0); + } + + private createNewPlayer(iframe: HTMLIFrameElement, options: SdkOptions) { + this.setIframeSrc(iframe, this.buildPlayerUrl(options)); + } + + private buildPlayerUrl(options: SdkOptions) { + if (!options.id) { + throw new Error("Missing id in options"); + } + + const url = this.iframeUrl + .replace("${id}", options.id) + .replace("${type}", options.live ? "live" : "vod"); + + return this.addParametersInIframeHash( + `${url}?${this.urlParametersFromOptions(options)}`, + options + ); + } + + private bindExistingPlayer(iframe: HTMLIFrameElement) { + this.setIframeSrc( + iframe, + this.addParametersInIframeHash(iframe.src, {} as SdkOptions) + ); + } + + private addParametersInIframeHash(url: string, options: SdkOptions) { + const addParameterInIframeHash = (parameter: string, value?: string) => { + const indexOfHash = url.indexOf("#"); + const parameterAndValue = value ? `${parameter}:${value}` : parameter; + + if (indexOfHash === -1) { + return `${url}#${parameterAndValue}`; + } + const beforeHash = url.substr(0, indexOfHash); + let afterHash = url.substr(indexOfHash + 1); + + afterHash = afterHash.replace(new RegExp(`${parameter}(:[^;]+)?;?`), ""); + + return `${beforeHash}#${parameterAndValue};${afterHash}`; }; - hideControls(controls?: ControlName[]) { - if(!controls) { - this.options.hideControls = true; - } - this.postMessage({ message: 'hideControls', controls }, undefined, true); - } - showControls(controls?: ControlName[]) { - if(!controls) { - this.options.hideControls = false; - } - this.postMessage({ message: 'showControls', controls }, undefined, true); - } - hideSubtitles() { - this.options.showSubtitles = false; - this.postMessage({ message: 'hideSubtitles' }); - } - showSubtitles() { - this.options.showSubtitles = true; - this.postMessage({ message: 'showSubtitles' }); - } - hideTitle() { - this.options.hideTitle = true; - this.postMessage({ message: 'hideTitle' }); - } - showTitle() { - this.options.hideTitle = false; - this.postMessage({ message: 'showTitle' }); - } - hidePoster() { - this.options.hidePoster = true; - this.postMessage({ message: 'hidePoster' }); - } - showPoster() { - this.options.hidePoster = false; - this.postMessage({ message: 'showPoster' }); - } - pause() { - this.postMessage({ message: 'pause' }); - } - mute() { - this.options.muted = true; - this.postMessage({ message: 'mute' }); - } - unmute() { - this.options.muted = false; - this.postMessage({ message: 'unmute' }); - } - seek(time: number) { - this.postMessage({ message: 'seek', seek: time }); - } - setCurrentTime(time: number) { - this.postMessage({ message: 'setCurrentTime', currentTime: time }); - } - setVolume(volume: number) { - this.postMessage({ message: 'setVolume', volume }); - } - setAutoplay(autoplay: boolean) { - this.options.autoplay = autoplay; - this.postMessage({ message: 'setAutoplay', autoplay }); - } - setLoop(loop: boolean) { - this.options.loop = loop; - this.postMessage({ message: 'setLoop', loop }); - } - setChromeless(chromeless: boolean) { - this.options.chromeless = chromeless; - this.postMessage({ message: 'setChromeless', chromeless }); - } - setPlaybackRate(rate: number) { - this.options.playbackRate = rate; - this.postMessage({ message: 'setPlaybackRate', rate }, undefined, true); - } - exitFullscreen() { - this.postMessage({ message: 'exitFullscreen' }, undefined, true); - } - requestFullscreen() { - this.postMessage({ message: 'requestFullscreen' }, undefined, true); - } - exitPictureInPicture() { - this.postMessage({ message: 'exitPictureInPicture' }, undefined, true); - } - requestPictureInPicture() { - this.postMessage({ message: 'requestPictureInPicture' }, undefined, true); - } - setTheme(theme: PlayerTheme) { - this.postMessage({ message: 'setTheme', theme }); - } - getPaused(callback?: (paused: boolean) => void): Promise { - return this.postMessage({ message: 'getPaused' }, callback); - } - getMuted(callback?: (muted: boolean) => void): Promise { - return this.postMessage({ message: 'getMuted' }, callback); - } - getPlaying(callback?: (muted: boolean) => void): Promise { - return this.postMessage({ message: 'getPlaying' }, callback); - } - getDuration(callback?: (duration: number) => void): Promise { - return this.postMessage({ message: 'getDuration' }, callback, true); - } - getCurrentTime(callback?: (currentTime: number) => void): Promise { - return this.postMessage({ message: 'getCurrentTime' }, callback); - } - getPlaybackRate(callback?: (rate: number) => void): Promise { - return this.postMessage({ message: 'getPlaybackRate' }, callback); - } - getVolume(callback?: (volume: number) => void): Promise { - return this.postMessage({ message: 'getVolume' }, callback); - } - getLoop(callback?: (loop: boolean) => void): Promise { - return this.postMessage({ message: 'getLoop' }, callback); - } - getVideoSize(callback?: (size: {width: number, height: number}) => void): Promise<{width: number, height: number}> { - return this.postMessage({ message: 'getVideoSize' }, callback, true); - } - isLiveStream(callback?: (isLiveStream: boolean) => void): Promise { - return this.postMessage({ message: 'isLiveStream' }, callback); - } + url = addParameterInIframeHash("sdkPlayerId", "" + this.sdkPlayerId); + url = addParameterInIframeHash("sdkOrigin", btoa(this.sdkOrigin)); + url = addParameterInIframeHash("api"); - addEventListener(event: K, callback: PlayerSdkEvent[K]) { - this.userEventListeners.push({ event, callback }); + if (options.hideControls === true) { + url = addParameterInIframeHash("hide-controls"); } - destroy() { - this.postMessage({ message: 'destroy' }); - setTimeout(() => this.iframe?.parentElement?.removeChild(this.iframe), 0); + if (options.chromeless === true) { + url = addParameterInIframeHash("chromeless"); } - private createNewPlayer(iframe: HTMLIFrameElement, options: SdkOptions) { - this.setIframeSrc(iframe, this.buildPlayerUrl(options)); + if (options.hidePoster === true) { + url = addParameterInIframeHash("hide-poster"); } - private buildPlayerUrl(options: SdkOptions) { - if (!options.id) { - throw new Error("Missing id in options"); - } - - const url = this.iframeUrl - .replace("${id}", options.id) - .replace("${type}", options.live ? "live" : "vod"); - - return this.addParametersInIframeHash(`${url}?${this.urlParametersFromOptions(options)}`, options); + if (options.loop === true) { + url = addParameterInIframeHash("loop"); } - private bindExistingPlayer(iframe: HTMLIFrameElement) { - this.setIframeSrc(iframe, this.addParametersInIframeHash(iframe.src, {} as SdkOptions)); + if (options.hideTitle === true) { + url = addParameterInIframeHash("hide-title"); } - private addParametersInIframeHash(url: string, options: SdkOptions) { - const addParameterInIframeHash = (parameter: string, value?: string) => { - const indexOfHash = url.indexOf("#"); - const parameterAndValue = value ? `${parameter}:${value}` : parameter; - - if (indexOfHash === -1) { - return `${url}#${parameterAndValue}`; - } - const beforeHash = url.substr(0, indexOfHash); - let afterHash = url.substr(indexOfHash + 1); - - afterHash = afterHash.replace(new RegExp(`${parameter}(:[^;]+)?;?`), ""); - - return `${beforeHash}#${parameterAndValue};${afterHash}`; - }; - - url = addParameterInIframeHash("sdkPlayerId", "" + this.sdkPlayerId); - url = addParameterInIframeHash("sdkOrigin", btoa(this.sdkOrigin)); - url = addParameterInIframeHash("api"); - - if (options.hideControls === true) { - url = addParameterInIframeHash("hide-controls"); - } - - if (options.chromeless === true) { - url = addParameterInIframeHash("chromeless"); - } - - if (options.hidePoster === true) { - url = addParameterInIframeHash("hide-poster"); - } - - if (options.loop === true) { - url = addParameterInIframeHash("loop"); - } - - if (options.hideTitle === true) { - url = addParameterInIframeHash("hide-title"); - } - - if (options.showSubtitles === true) { - url = addParameterInIframeHash("show-subtitles"); - } - - if(options.minimalQuality !== undefined) { - url = addParameterInIframeHash(`min-quality:${encodeURIComponent(options.minimalQuality)}`); - } - - if(options.maximalQuality !== undefined) { - url = addParameterInIframeHash(`max-quality:${encodeURIComponent(options.maximalQuality)}`); - } - - if(options.ads?.adTagUrl) { - url = addParameterInIframeHash(`adTagUrl:${encodeURIComponent(options.ads?.adTagUrl)}`); - } - - if(!isNaN(parseInt(""+options.sequence?.end, 10)) && !isNaN(parseInt(""+options.sequence?.start, 10))) { - url = addParameterInIframeHash(`t=${options.sequence?.start},${options.sequence?.end}`); - } - - return url; + if (options.showSubtitles === true) { + url = addParameterInIframeHash("show-subtitles"); } - private urlParametersFromOptions(options: SdkOptions) { - const allowedKeys = ["id", "live", "autoplay", "muted", "metadata", "hideControls", "hidePoster", - "chromeless", "loop", "hideTitle", "iframeUrl", "token", "showSubtitles", "ts","avh"]; - - const optionsAsAny = options as any; - optionsAsAny.ts = new Date().getTime(); - - if(options.privateSession) { - optionsAsAny.avh = options.privateSession; - } - - return Object.keys(options).map((key: string) => { - if(allowedKeys.indexOf(key) === -1) { - return; - } - if (key === "metadata" && typeof optionsAsAny[key] === "object") { - const metadata = optionsAsAny[key]; - return Object.keys(metadata).map((metadataName: string) => { - return "metadata[" + metadataName + "]=" + metadata[metadataName]; - }).join("&"); - } - return key + '=' + optionsAsAny[key]; - }).join('&'); + if (options.minimalQuality !== undefined) { + url = addParameterInIframeHash( + `min-quality:${encodeURIComponent(options.minimalQuality)}` + ); } - private onEvent(data: PlayerEvent) { - const userData = { ...data } as any; - delete userData.type; - delete userData.sdkPlayerId; - - this.userEventListeners - .filter(uel => uel.event === data.type) - .forEach(uel => uel.callback(userData)); - - switch (data.type) { - case 'ready': - this.onVideoReady(); - break; - case 'sdkSync': - this.onSdkInSync(); - break; - } + if (options.maximalQuality !== undefined) { + url = addParameterInIframeHash( + `max-quality:${encodeURIComponent(options.maximalQuality)}` + ); } - private onSdkInSync() { - if (!this.sdkInSync) { - this.sdkInSync = true; - this.onceSdkInSyncCallbacks.forEach(cb => { - cb(); - }); - } + if (options.ads?.adTagUrl) { + url = addParameterInIframeHash( + `adTagUrl:${encodeURIComponent(options.ads?.adTagUrl)}` + ); } - private onVideoReady() { - if(!this.currentVideoReady) { - this.currentVideoReady = true; - this.onceVideoReadyCallbacks.forEach(cb => { - cb(); - }); - } + if ( + !isNaN(parseInt("" + options.sequence?.end, 10)) && + !isNaN(parseInt("" + options.sequence?.start, 10)) + ) { + url = addParameterInIframeHash( + `t=${options.sequence?.start},${options.sequence?.end}` + ); } - private postMessage(message: any, callback?: (arg: T) => void, requireVideoReady: boolean = false): Promise { - - return new Promise((resolve, reject): void => { - - if (!this.playerOrigin || !this.iframe?.contentWindow) { - reject(); - return; - } - - const messageWithPlayerId = { - ...message, - sdkPlayerId: this.sdkPlayerId - } - - const callbackId = this.makeId(16); - this.postMessageCallbacks[callbackId] = (res: T) => { - resolve(res as T); - if (!!callback) { - callback(res); - } - }; - messageWithPlayerId.callbackId = callbackId; - - if(!this.currentVideoReady && requireVideoReady) { - this.onceVideoReadyCallbacks.push(() => this.playerOrigin && this.iframe?.contentWindow?.postMessage(messageWithPlayerId, this.playerOrigin)); - return; - } - - if (this.sdkInSync && !!this.playerOrigin) { - this.iframe.contentWindow.postMessage(messageWithPlayerId, this.playerOrigin); - return; - } - - this.onceSdkInSyncCallbacks.push(() => this.playerOrigin && this.iframe?.contentWindow?.postMessage(messageWithPlayerId, this.playerOrigin)); - }); - } + return url; + } - private makeId(length: number) { - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - const charactersLength = characters.length; - let result = ''; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - } - return result; - } + private urlParametersFromOptions(options: SdkOptions) { + const allowedKeys = [ + "id", + "live", + "autoplay", + "muted", + "metadata", + "hideControls", + "hidePoster", + "chromeless", + "loop", + "hideTitle", + "iframeUrl", + "token", + "showSubtitles", + "ts", + "avh", + ]; - private createIframe(target: Element) { - const ifr = document.createElement('iframe'); - ifr.style.height = "100%"; - ifr.style.width = "100%"; - ifr.allowFullscreen = true; - ifr.allow = "autoplay"; - target.appendChild(ifr); - return ifr; - } + const optionsAsAny = options as any; + optionsAsAny.ts = new Date().getTime(); - private setIframeSrc(iframe: HTMLIFrameElement, url: string) { - iframe.src = url; + if (options.privateSession) { + optionsAsAny.avh = options.privateSession; } -} \ No newline at end of file + + return Object.keys(options) + .map((key: string) => { + if (allowedKeys.indexOf(key) === -1) { + return; + } + if (key === "metadata" && typeof optionsAsAny[key] === "object") { + const metadata = optionsAsAny[key]; + return Object.keys(metadata) + .map((metadataName: string) => { + return "metadata[" + metadataName + "]=" + metadata[metadataName]; + }) + .join("&"); + } + return key + "=" + optionsAsAny[key]; + }) + .join("&"); + } + + private onEvent(data: PlayerEvent) { + const userData = { ...data } as any; + delete userData.type; + delete userData.sdkPlayerId; + + this.userEventListeners + .filter((uel) => uel.event === data.type) + .forEach((uel) => uel.callback(userData)); + + switch (data.type) { + case "ready": + this.onVideoReady(); + break; + case "sdkSync": + this.onSdkInSync(); + break; + } + } + + private onSdkInSync() { + if (!this.sdkInSync) { + this.sdkInSync = true; + this.onceSdkInSyncCallbacks.forEach((cb) => { + cb(); + }); + } + } + + private onVideoReady() { + if (!this.currentVideoReady) { + this.currentVideoReady = true; + this.onceVideoReadyCallbacks.forEach((cb) => { + cb(); + }); + } + } + + private postMessage( + message: any, + callback?: (arg: T) => void, + requireVideoReady: boolean = false + ): Promise { + return new Promise((resolve, reject): void => { + if (!this.playerOrigin || !this.iframe?.contentWindow) { + reject(); + return; + } + + const messageWithPlayerId = { + ...message, + sdkPlayerId: this.sdkPlayerId, + }; + + const callbackId = this.makeId(16); + this.postMessageCallbacks[callbackId] = (res: T) => { + resolve(res as T); + if (!!callback) { + callback(res); + } + }; + messageWithPlayerId.callbackId = callbackId; + + if (!this.currentVideoReady && requireVideoReady) { + this.onceVideoReadyCallbacks.push( + () => + this.playerOrigin && + this.iframe?.contentWindow?.postMessage( + messageWithPlayerId, + this.playerOrigin + ) + ); + return; + } + + if (this.sdkInSync && !!this.playerOrigin) { + this.iframe.contentWindow.postMessage( + messageWithPlayerId, + this.playerOrigin + ); + return; + } + + this.onceSdkInSyncCallbacks.push( + () => + this.playerOrigin && + this.iframe?.contentWindow?.postMessage( + messageWithPlayerId, + this.playerOrigin + ) + ); + }); + } + + private makeId(length: number) { + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const charactersLength = characters.length; + let result = ""; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; + } + + private createIframe(target: Element) { + const ifr = document.createElement("iframe"); + ifr.style.height = "100%"; + ifr.style.width = "100%"; + ifr.allowFullscreen = true; + ifr.allow = "autoplay"; + target.appendChild(ifr); + return ifr; + } + + private setIframeSrc(iframe: HTMLIFrameElement, url: string) { + iframe.src = url; + } +} diff --git a/package-lock.json b/package-lock.json index c2626c5..e3cbf4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@api.video/player-sdk", - "version": "1.2.28", + "version": "1.2.29", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@api.video/player-sdk", - "version": "1.2.28", + "version": "1.2.29", "license": "MIT", "dependencies": { "core-js": "^3.8.3", diff --git a/package.json b/package.json index 5f01777..b21c855 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@api.video/player-sdk", - "version": "1.2.28", + "version": "1.2.29", "description": "api.video player SDK", "repository": { "type": "git",