diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 977c3b58..78c2d2ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,7 @@ on: push: branches: - main + - develop paths-ignore: - '**.md' pull_request: diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 00000000..ebf7abba --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,68 @@ +name: Create release PR +on: + workflow_dispatch: + inputs: + version-bump: + description: 'Version bump' + required: true + default: 'patch' + type: choice + options: + - major + - minor + - patch + version: + description: 'Custom version' + required: false + type: string +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + strategy: + matrix: + node-version: [20] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + steps: + - name: Create app token + uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.THEOPLAYER_BOT_APP_ID }} + private-key: ${{ secrets.THEOPLAYER_BOT_PRIVATE_KEY }} + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ steps.app-token.outputs.token }} + - name: Configure Git user + run: | + git config user.name 'theoplayer-bot[bot]' + git config user.email '873105+theoplayer-bot[bot]@users.noreply.github.com' + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci --workspaces + - name: Bump version + shell: bash + run: | + npm version --no-git-tag-version ${{ inputs.version || inputs.version-bump }} + echo "npm_version=$(jq -r .version package.json)" >> "$GITHUB_ENV" + - name: Push to release branch + shell: bash + run: | + git commit -a -m $npm_version + git push origin "HEAD:release/$npm_version" + - name: Create pull request + shell: bash + run: | + gh pr create \ + --base main \ + --head "release/$npm_version" \ + --title "Release $npm_version" \ + --body "This PR will publish version $npm_version." + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0769512c..5897cc26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ sidebar_custom_props: { 'icon': '📰' } > - 🏠 Internal > - 💅 Polish +## v1.8.1 (2024-04-18) + +- 🐛 Fixed `ui.player.destroy()` not working. ([#59](https://github.com/THEOplayer/web-ui/issues/59), [#62](https://github.com/THEOplayer/web-ui/pull/62)) + ## v1.8.0 (2024-04-12) - 💥 **Breaking Change**: This project now requires THEOplayer version 7.0.0 or higher. ([#60](https://github.com/THEOplayer/web-ui/pull/60)) diff --git a/package-lock.json b/package-lock.json index 8ad46315..f602bd47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@theoplayer/web-ui", - "version": "1.8.0", + "version": "1.8.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@theoplayer/web-ui", - "version": "1.8.0", + "version": "1.8.1", "license": "MIT", "workspaces": [ ".", @@ -5570,11 +5570,11 @@ }, "react": { "name": "@theoplayer/react-ui", - "version": "1.8.0", + "version": "1.8.1", "license": "MIT", "dependencies": { "@lit/react": "^1.0.3", - "@theoplayer/web-ui": "^1.8.0" + "@theoplayer/web-ui": "^1.8.1" }, "devDependencies": { "@rollup/plugin-json": "^6.1.0", @@ -6209,7 +6209,7 @@ "@swc/cli": "^0.1.62", "@swc/core": "^1.3.89", "@swc/helpers": "^0.5.2", - "@theoplayer/web-ui": "^1.8.0", + "@theoplayer/web-ui": "^1.8.1", "@types/react": "^18.2.48", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -6864,7 +6864,7 @@ "@swc/cli": "^0.1.62", "@swc/core": "^1.3.89", "@swc/helpers": "^0.5.2", - "@theoplayer/web-ui": "^1.8.0", + "@theoplayer/web-ui": "^1.8.1", "@types/react": "^18.2.48", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/package.json b/package.json index f407d855..2fbbf652 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@theoplayer/web-ui", - "version": "1.8.0", + "version": "1.8.1", "description": "UI component library for the THEOplayer Web SDK", "main": "dist/THEOplayerUI.js", "module": "dist/THEOplayerUI.mjs", diff --git a/react/CHANGELOG.md b/react/CHANGELOG.md index 3a86c1b1..a3c0c546 100644 --- a/react/CHANGELOG.md +++ b/react/CHANGELOG.md @@ -15,6 +15,11 @@ sidebar_custom_props: { 'icon': '📰' } > - 🏠 Internal > - 💅 Polish +## v1.8.1 (2024-04-18) + +- 🐛 Fixed backing THEOplayer not always being destroyed on unmount. ([#59](https://github.com/THEOplayer/web-ui/issues/59), [#62](https://github.com/THEOplayer/web-ui/pull/62)) +- 🏠 See changes to [Open Video UI for Web v1.8.1](https://github.com/THEOplayer/web-ui/blob/v1.8.1/CHANGELOG.md) + ## v1.8.0 (2024-04-12) - 💥 **Breaking Change**: This project now requires THEOplayer version 7.0.0 or higher. ([#60](https://github.com/THEOplayer/web-ui/pull/60)) diff --git a/react/package.json b/react/package.json index 5aba0482..61ef324b 100644 --- a/react/package.json +++ b/react/package.json @@ -1,6 +1,6 @@ { "name": "@theoplayer/react-ui", - "version": "1.8.0", + "version": "1.8.1", "description": "React component library for the THEOplayer Web SDK", "main": "dist/THEOplayerReactUI.js", "module": "dist/THEOplayerReactUI.mjs", @@ -52,7 +52,7 @@ }, "dependencies": { "@lit/react": "^1.0.3", - "@theoplayer/web-ui": "^1.8.0" + "@theoplayer/web-ui": "^1.8.1" }, "peerDependencies": { "@types/react": "^16.3.0 || ^17 || ^18", diff --git a/react/src/util.ts b/react/src/util.ts index 0450ede2..dfca638f 100644 --- a/react/src/util.ts +++ b/react/src/util.ts @@ -26,5 +26,16 @@ export function usePlayer( onReady?.(player); }, [player, onReady]); + // Destroy player on unmount. + useEffect(() => { + return () => { + try { + player?.destroy(); + } catch { + // Ignore, probably already destroyed. + } + }; + }, [player]); + return player; } diff --git a/src/UIContainer.ts b/src/UIContainer.ts index e5d038ca..e1bb2e3c 100644 --- a/src/UIContainer.ts +++ b/src/UIContainer.ts @@ -438,30 +438,12 @@ export class UIContainer extends HTMLElement { this._player.muted = this.muted; this._player.autoplay = this.autoplay; - for (const receiver of this._stateReceivers) { - if (receiver[StateReceiverProps].indexOf('player') >= 0) { - receiver.player = this._player; - } - } - this._updateAspectRatio(); this._updateError(); this._updatePausedAndEnded(); this._updateCasting(); - this._player.addEventListener('resize', this._updateAspectRatio); - this._player.addEventListener(['error', 'sourcechange', 'emptied'], this._updateError); - this._player.addEventListener('volumechange', this._updateMuted); - this._player.addEventListener('play', this._onPlay); - this._player.addEventListener('pause', this._onPause); - this._player.addEventListener(['ended', 'emptied'], this._updatePausedAndEnded); - this._player.addEventListener(['durationchange', 'sourcechange', 'emptied'], this._updateStreamType); - this._player.addEventListener('ratechange', this._updatePlaybackRate); - this._player.addEventListener('sourcechange', this._onSourceChange); - this._player.theoLive?.addEventListener('publicationloadstart', this._onSourceChange); - this._player.videoTracks.addEventListener(['addtrack', 'removetrack', 'change'], this._updateActiveVideoTrack); - this._player.cast?.addEventListener('castingchange', this._updateCasting); - this._player.addEventListener(['durationchange', 'sourcechange', 'emptied'], this._updatePlayingAd); - this._player.ads?.addEventListener(['adbreakbegin', 'adbreakend', 'adbegin', 'adend', 'adskip'], this._updatePlayingAd); + this.addPlayerListeners_(this._player); + this.propagatePlayerToAllReceivers_(); this.dispatchEvent(createCustomEvent(READY_EVENT)); } @@ -470,10 +452,6 @@ export class UIContainer extends HTMLElement { this._resizeObserver?.disconnect(); this._mutationObserver.disconnect(); this.shadowRoot!.removeEventListener('slotchange', this._onSlotChange); - for (const receiver of this._stateReceivers) { - this.removeStateFromReceiver_(receiver); - } - this._stateReceivers.length = 0; this._menuGroup.removeEventListener(CLOSE_MENU_EVENT, this._onCloseMenu); this._menuGroup.removeEventListener(MENU_CHANGE_EVENT, this._onMenuChange); @@ -491,9 +469,13 @@ export class UIContainer extends HTMLElement { this.removeEventListener('mouseleave', this._onMouseLeave); if (this._player) { + this.removePlayerListeners_(this._player); this._player.destroy(); this._player = undefined; + this.propagatePlayerToAllReceivers_(); } + + this._stateReceivers.length = 0; } attributeChangedCallback(attrName: string, oldValue: any, newValue: any): void { @@ -628,6 +610,15 @@ export class UIContainer extends HTMLElement { } } + private propagatePlayerToAllReceivers_(): void { + for (const receiver of this._stateReceivers) { + const receiverProps = receiver[StateReceiverProps]; + if (receiverProps.indexOf('player') >= 0) { + receiver.player = this._player; + } + } + } + private removeStateFromReceiver_(receiver: StateReceiverElement): void { const receiverProps = receiver[StateReceiverProps]; if (receiverProps.indexOf('player') >= 0) { @@ -1065,6 +1056,56 @@ export class UIContainer extends HTMLElement { } } }; + + private addPlayerListeners_(player: ChromelessPlayer): void { + player.addEventListener('destroy', this._onDestroy); + player.addEventListener('resize', this._updateAspectRatio); + player.addEventListener(['error', 'sourcechange', 'emptied'], this._updateError); + player.addEventListener('volumechange', this._updateMuted); + player.addEventListener('play', this._onPlay); + player.addEventListener('pause', this._onPause); + player.addEventListener(['ended', 'emptied'], this._updatePausedAndEnded); + player.addEventListener(['durationchange', 'sourcechange', 'emptied'], this._updateStreamType); + player.addEventListener('ratechange', this._updatePlaybackRate); + player.addEventListener('sourcechange', this._onSourceChange); + player.addEventListener(['durationchange', 'sourcechange', 'emptied'], this._updatePlayingAd); + + player.theoLive?.addEventListener('publicationloadstart', this._onSourceChange); + player.videoTracks.addEventListener(['addtrack', 'removetrack', 'change'], this._updateActiveVideoTrack); + player.cast?.addEventListener('castingchange', this._updateCasting); + player.ads?.addEventListener(['adbreakbegin', 'adbreakend', 'adbegin', 'adend', 'adskip'], this._updatePlayingAd); + } + + private removePlayerListeners_(player: ChromelessPlayer): void { + player.removeEventListener('destroy', this._onDestroy); + player.removeEventListener('resize', this._updateAspectRatio); + player.removeEventListener(['error', 'sourcechange', 'emptied'], this._updateError); + player.removeEventListener('volumechange', this._updateMuted); + player.removeEventListener('play', this._onPlay); + player.removeEventListener('pause', this._onPause); + player.removeEventListener(['ended', 'emptied'], this._updatePausedAndEnded); + player.removeEventListener(['durationchange', 'sourcechange', 'emptied'], this._updateStreamType); + player.removeEventListener('ratechange', this._updatePlaybackRate); + player.removeEventListener('sourcechange', this._onSourceChange); + player.removeEventListener(['durationchange', 'sourcechange', 'emptied'], this._updatePlayingAd); + + try { + player.theoLive?.removeEventListener('publicationloadstart', this._onSourceChange); + player.videoTracks.removeEventListener(['addtrack', 'removetrack', 'change'], this._updateActiveVideoTrack); + player.cast?.removeEventListener('castingchange', this._updateCasting); + player.ads?.removeEventListener(['adbreakbegin', 'adbreakend', 'adbegin', 'adend', 'adskip'], this._updatePlayingAd); + } catch { + // Ignore errors from accessing player.ads when the player is already destroyed. + } + } + + private readonly _onDestroy = (): void => { + if (this._player) { + this.removePlayerListeners_(this._player); + this._player = undefined; + this.propagatePlayerToAllReceivers_(); + } + }; } customElements.define('theoplayer-ui', UIContainer); diff --git a/src/components/ChromecastDisplay.ts b/src/components/ChromecastDisplay.ts index dc4d0426..893f28cd 100644 --- a/src/components/ChromecastDisplay.ts +++ b/src/components/ChromecastDisplay.ts @@ -2,7 +2,7 @@ import * as shadyCss from '@webcomponents/shadycss'; import chromecastDisplayCss from './ChromecastDisplay.css'; import chromecastIcon from '../icons/chromecast-48px.svg'; import { StateReceiverMixin } from './StateReceiverMixin'; -import type { ChromelessPlayer } from 'theoplayer/chromeless'; +import type { Chromecast, ChromelessPlayer } from 'theoplayer/chromeless'; import { setTextContent } from '../util/CommonUtils'; import { Attribute } from '../util/Attribute'; import { createTemplate } from '../util/TemplateUtils'; @@ -27,6 +27,7 @@ const CAST_EVENTS = ['statechange'] as const; export class ChromecastDisplay extends StateReceiverMixin(HTMLElement, ['player']) { private readonly _receiverNameEl: HTMLElement; private _player: ChromelessPlayer | undefined; + private _castApi: Chromecast | undefined; constructor() { super(); @@ -61,10 +62,15 @@ export class ChromecastDisplay extends StateReceiverMixin(HTMLElement, ['player' if (this._player === player) { return; } - this._player?.cast?.chromecast?.removeEventListener(CAST_EVENTS, this._updateFromPlayer); + if (this._castApi !== undefined) { + this._castApi.removeEventListener(CAST_EVENTS, this._updateFromPlayer); + } this._player = player; + this._castApi = player?.cast?.chromecast; this._updateFromPlayer(); - this._player?.cast?.chromecast?.addEventListener(CAST_EVENTS, this._updateFromPlayer); + if (this._castApi !== undefined) { + this._castApi.addEventListener(CAST_EVENTS, this._updateFromPlayer); + } } private readonly _updateFromPlayer = () => { diff --git a/src/components/GestureReceiver.ts b/src/components/GestureReceiver.ts index dfc48ef8..81fbfca9 100644 --- a/src/components/GestureReceiver.ts +++ b/src/components/GestureReceiver.ts @@ -16,6 +16,7 @@ const template = createTemplate('theoplayer-gesture-receiver', `