From 6459fad8c3c6247999110fa5b7e701f5a7587a53 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 14:09:54 +0000 Subject: [PATCH 1/2] feat: add contentprotectionerror player event on all platforms Co-Authored-By: tom.vanlaerhoven --- CHANGELOG.md | 4 ++ .../java/com/theoplayer/PlayerEventEmitter.kt | 12 ++++ ios/THEOplayerRCTBridge.m | 1 + ios/THEOplayerRCTMainEventHandler.swift | 27 +++++++++ ios/THEOplayerRCTView.swift | 6 ++ src/api/error/ContentProtectionError.ts | 58 +++++++++++++++++++ src/api/error/barrel.ts | 1 + src/api/event/PlayerEvent.ts | 17 +++++- src/api/player/PlayerEventMap.ts | 7 +++ src/internal/THEOplayerView.tsx | 8 +++ src/internal/adapter/WebEventForwarder.ts | 18 ++++++ src/internal/adapter/event/PlayerEvents.ts | 8 +++ .../adapter/event/native/NativePlayerEvent.ts | 5 ++ 13 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 src/api/error/ContentProtectionError.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e89c3f8..1b66ba478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- Added `contentprotectionerror` player event on Web, Android, and iOS platforms. + ## [11.3.0] - 26-06-18 ### Added diff --git a/android/src/main/java/com/theoplayer/PlayerEventEmitter.kt b/android/src/main/java/com/theoplayer/PlayerEventEmitter.kt index 7bee2bfde..0ea593b49 100644 --- a/android/src/main/java/com/theoplayer/PlayerEventEmitter.kt +++ b/android/src/main/java/com/theoplayer/PlayerEventEmitter.kt @@ -85,6 +85,7 @@ private const val EVENT_THEOLIVE_EVENT = "onNativeTHEOliveEvent" private const val EVENT_THEOADS_EVENT = "onNativeTHEOadsEvent" private const val EVENT_PRESENTATIONMODECHANGE = "onNativePresentationModeChange" private const val EVENT_VOLUMECHANGE = "onNativeVolumeChange" +private const val EVENT_CONTENTPROTECTIONERROR = "onNativeContentProtectionError" private const val EVENT_DIMENSIONCHANGE = "onNativeDimensionChange" private const val EVENT_VIDEORESIZE = "onNativeVideoResize" @@ -132,6 +133,7 @@ class PlayerEventEmitter internal constructor( EVENT_THEOADS_EVENT, EVENT_PRESENTATIONMODECHANGE, EVENT_VOLUMECHANGE, + EVENT_CONTENTPROTECTIONERROR, EVENT_DIMENSIONCHANGE, EVENT_VIDEORESIZE ) @@ -171,6 +173,7 @@ class PlayerEventEmitter internal constructor( EVENT_THEOADS_EVENT, EVENT_PRESENTATIONMODECHANGE, EVENT_VOLUMECHANGE, + EVENT_CONTENTPROTECTIONERROR, EVENT_DIMENSIONCHANGE, EVENT_VIDEORESIZE ) @@ -246,6 +249,8 @@ class PlayerEventEmitter internal constructor( EventListener { event: PresentationModeChange -> onPresentationModeChange(event) } playerListeners[PlayerEventTypes.VOLUMECHANGE] = EventListener { event: VolumeChangeEvent -> onVolumeChange(event) } + playerListeners[PlayerEventTypes.CONTENTPROTECTIONERROR] = + EventListener { event: ContentProtectionErrorEvent -> onContentProtectionError(event) } playerListeners[PlayerEventTypes.RESIZE] = EventListener { event: ResizeEvent -> onResize(event) } textTrackListeners[TextTrackListEventTypes.ADDTRACK] = @@ -479,6 +484,13 @@ class PlayerEventEmitter internal constructor( ) } + private fun onContentProtectionError(event: ContentProtectionErrorEvent) { + receiveEvent( + EVENT_CONTENTPROTECTIONERROR, + PayloadBuilder().error(event.errorObject.code.id.toString(), event.errorObject.message).build() + ) + } + private fun onResize(event: ResizeEvent) { receiveEvent( EVENT_VIDEORESIZE, diff --git a/ios/THEOplayerRCTBridge.m b/ios/THEOplayerRCTBridge.m index c3715b89d..735c81569 100644 --- a/ios/THEOplayerRCTBridge.m +++ b/ios/THEOplayerRCTBridge.m @@ -47,6 +47,7 @@ @interface RCT_EXTERN_MODULE(THEOplayerRCTViewManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(onNativeDeviceOrientationChanged, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onNativePlayerReady, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onNativePresentationModeChange, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onNativeContentProtectionError, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onNativeDimensionChange, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onNativeVideoResize, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onNativeAdEvent, RCTDirectEventBlock); diff --git a/ios/THEOplayerRCTMainEventHandler.swift b/ios/THEOplayerRCTMainEventHandler.swift index 3afab8d39..c366f463b 100644 --- a/ios/THEOplayerRCTMainEventHandler.swift +++ b/ios/THEOplayerRCTMainEventHandler.swift @@ -30,6 +30,7 @@ public class THEOplayerRCTMainEventHandler { var onNativeRateChange: RCTDirectEventBlock? var onNativeWaiting: RCTDirectEventBlock? var onNativeCanPlay: RCTDirectEventBlock? + var onNativeContentProtectionError: RCTDirectEventBlock? var onNativeDimensionChange: RCTDirectEventBlock? var onNativeVideoResize: RCTDirectEventBlock? @@ -54,6 +55,7 @@ public class THEOplayerRCTMainEventHandler { private var rateChangeListener: EventListener? private var waitingListener: EventListener? private var canPlayListener: EventListener? + private var contentProtectionErrorListener: EventListener? private var videoResizeListener: EventListener? // MARK: player observer @@ -326,6 +328,25 @@ public class THEOplayerRCTMainEventHandler { } if DEBUG_EVENTHANDLER { PrintUtils.printLog(logText: "[NATIVE] CanPlay listener attached to THEOplayer") } + // CONTENT_PROTECTION_ERROR + self.contentProtectionErrorListener = player.addEventListener(type: PlayerEventTypes.CONTENT_PROTECTION_ERROR) { [weak self] event in + if DEBUG_THEOPLAYER_EVENTS { PrintUtils.printLog(logText: "[NATIVE] Received CONTENT_PROTECTION_ERROR event from THEOplayer") } + if let forwardedContentProtectionErrorEvent = self?.onNativeContentProtectionError, + let errorObject = event.errorObject { + let errorCodeString = String(errorObject.code.rawValue) + let errorCodeMessage = errorObject.message + forwardedContentProtectionErrorEvent( + [ + "error": [ + "errorCode": errorCodeString, + "errorMessage": errorCodeMessage + ] + ] + ) + } + } + if DEBUG_EVENTHANDLER { PrintUtils.printLog(logText: "[NATIVE] ContentProtectionError listener attached to THEOplayer") } + // RESIZE self.videoResizeListener = player.addEventListener(type: PlayerEventTypes.RESIZE) { [weak self, weak player] event in if DEBUG_THEOPLAYER_EVENTS { PrintUtils.printLog(logText: "[NATIVE] Received RESIZE event from THEOplayer") } @@ -480,6 +501,12 @@ public class THEOplayerRCTMainEventHandler { self.dimensionChangeObserver?.invalidate() self.dimensionChangeObserver = nil + // CONTENT_PROTECTION_ERROR + if let contentProtectionErrorListener = self.contentProtectionErrorListener { + player.removeEventListener(type: PlayerEventTypes.CONTENT_PROTECTION_ERROR, listener: contentProtectionErrorListener) + if DEBUG_EVENTHANDLER { PrintUtils.printLog(logText: "[NATIVE] ContentProtectionError listener dettached from THEOplayer") } + } + // RESIZE if let videoResizeListener = self.videoResizeListener { player.removeEventListener(type: PlayerEventTypes.RESIZE, listener: videoResizeListener) diff --git a/ios/THEOplayerRCTView.swift b/ios/THEOplayerRCTView.swift index d679581f8..84764ea17 100644 --- a/ios/THEOplayerRCTView.swift +++ b/ios/THEOplayerRCTView.swift @@ -470,6 +470,12 @@ public class THEOplayerRCTView: UIView { if DEBUG_VIEW { PrintUtils.printLog(logText: "[NATIVE] nativeCanPlay prop set.") } } + @objc(setOnNativeContentProtectionError:) + func setOnNativeContentProtectionError(nativeContentProtectionError: @escaping RCTDirectEventBlock) { + self.mainEventHandler.onNativeContentProtectionError = nativeContentProtectionError + if DEBUG_VIEW { PrintUtils.printLog(logText: "[NATIVE] nativeContentProtectionError prop set.") } + } + @objc(setOnNativeDimensionChange:) func setOnNativeDimensionChange(nativeDimensionChange: @escaping RCTDirectEventBlock) { self.mainEventHandler.onNativeDimensionChange = nativeDimensionChange diff --git a/src/api/error/ContentProtectionError.ts b/src/api/error/ContentProtectionError.ts new file mode 100644 index 000000000..5a4d948ef --- /dev/null +++ b/src/api/error/ContentProtectionError.ts @@ -0,0 +1,58 @@ +/** + * An error related to content protection. + * + * @category Errors + * @category Content Protection + * @public + */ +export interface ContentProtectionErrorObject { + /** + * The error code. + */ + readonly errorCode: string; + + /** + * The error message. + */ + readonly errorMessage: string; + + /** + * The URL that was used in the request. + * + * @remarks + * Only available for certificate or license request errors. + */ + readonly url?: string; + + /** + * The status code from the HTTP response. + * + * @remarks + * Only available for certificate or license request errors. + */ + readonly status?: number; + + /** + * The status text from the HTTP response. + * + * @remarks + * Only available for certificate or license request errors. + */ + readonly statusText?: string; + + /** + * The body contained in the HTTP response. + * + * @remarks + * Only available for certificate or license request errors. + */ + readonly response?: string; + + /** + * The internal error code from the CDM. + * + * @remarks + * Only available for internal CDM errors. + */ + readonly systemCode?: number; +} diff --git a/src/api/error/barrel.ts b/src/api/error/barrel.ts index 501369e89..ea0c18a1e 100644 --- a/src/api/error/barrel.ts +++ b/src/api/error/barrel.ts @@ -1,2 +1,3 @@ export * from './PlayerError'; export * from './ChromecastError'; +export * from './ContentProtectionError'; diff --git a/src/api/event/PlayerEvent.ts b/src/api/event/PlayerEvent.ts index 5e2d93e73..6715c9bbc 100644 --- a/src/api/event/PlayerEvent.ts +++ b/src/api/event/PlayerEvent.ts @@ -1,4 +1,4 @@ -import type { MediaTrack, PlayerError, PlayerEventType, PresentationMode, TextTrack, TypedSource } from 'react-native-theoplayer'; +import type { MediaTrack, PlayerError, PlayerEventType, PresentationMode, TextTrack, TypedSource, ContentProtectionErrorObject } from 'react-native-theoplayer'; import type { TimeRange } from '../timeranges/TimeRange'; import type { Event } from './Event'; @@ -297,3 +297,18 @@ export interface SeekedEvent extends Event { */ readonly currentTime: number; } + +/** + * Dispatched when an error related to content protection occurs. + * + * @category Events + * @category Player + * @category Content Protection + * @public + */ +export interface ContentProtectionErrorEvent extends Event { + /** + * An error object containing additional information about the content protection error. + */ + readonly error: ContentProtectionErrorObject; +} diff --git a/src/api/player/PlayerEventMap.ts b/src/api/player/PlayerEventMap.ts index 65b31125d..ba5ba4472 100644 --- a/src/api/player/PlayerEventMap.ts +++ b/src/api/player/PlayerEventMap.ts @@ -1,5 +1,6 @@ import type { Event } from '../event/Event'; import type { + ContentProtectionErrorEvent, CurrentSourceChangeEvent, DimensionChangeEvent, DurationChangeEvent, @@ -63,6 +64,7 @@ export enum PlayerEventType { DESTROY = 'destroy', DIMENSION_CHANGE = 'dimensionchange', VIDEO_RESIZE = 'videoresize', + CONTENT_PROTECTION_ERROR = 'contentprotectionerror', } /** @@ -242,4 +244,9 @@ export interface PlayerEventMap { * Dispatched when the video size changes. */ [PlayerEventType.VIDEO_RESIZE]: VideoResizeEvent; + + /** + * Dispatched when an error related to content protection occurs. + */ + [PlayerEventType.CONTENT_PROTECTION_ERROR]: ContentProtectionErrorEvent; } diff --git a/src/internal/THEOplayerView.tsx b/src/internal/THEOplayerView.tsx index b9a428d0c..884f40e49 100644 --- a/src/internal/THEOplayerView.tsx +++ b/src/internal/THEOplayerView.tsx @@ -23,6 +23,7 @@ import { DefaultAirplayStateChangeEvent, DefaultChromecastChangeEvent, DefaultChromecastErrorEvent, + DefaultContentProtectionErrorEvent, DefaultCurrentSourceChangeEvent, DefaultDimensionChangeEvent, DefaultDurationChangeEvent, @@ -52,6 +53,7 @@ import type { } from './adapter/event/native/NativeTrackEvent'; import { toMediaTrackType, toMediaTrackTypeEventType, toTextTrackEventType, toTrackListEventType } from './adapter/event/native/NativeTrackEvent'; import { + NativeContentProtectionErrorEvent, NativeCurrentSourceChangeEvent, NativeDurationChangeEvent, NativeErrorEvent, @@ -116,6 +118,7 @@ interface THEOplayerRCTViewProps { onNativeCastEvent: (event: NativeSyntheticEvent) => void; onNativePresentationModeChange: (event: NativeSyntheticEvent) => void; onNativeDeviceOrientationChanged: () => void; + onNativeContentProtectionError: (event: NativeSyntheticEvent) => void; onNativeDimensionChange: (event: NativeSyntheticEvent) => void; onNativeVideoResize: (event: NativeSyntheticEvent) => void; } @@ -422,6 +425,10 @@ export class THEOplayerView extends PureComponent) => { + this._facade?.dispatchEvent(new DefaultContentProtectionErrorEvent(event.nativeEvent.error)); + }; + private _onVideoResize = (event: NativeSyntheticEvent) => { this._facade?.dispatchEvent(new DefaultVideoResizeEvent(event.nativeEvent.videoWidth, event.nativeEvent.videoHeight)); }; @@ -503,6 +510,7 @@ export class THEOplayerView extends PureComponent diff --git a/src/internal/adapter/WebEventForwarder.ts b/src/internal/adapter/WebEventForwarder.ts index b6d3c63ac..ca2fa832a 100644 --- a/src/internal/adapter/WebEventForwarder.ts +++ b/src/internal/adapter/WebEventForwarder.ts @@ -6,6 +6,7 @@ import type { CastStateChangeEvent, ChromecastErrorEvent, ChromelessPlayer, + ContentProtectionErrorEvent as NativeContentProtectionErrorEvent, CurrentSourceChangeEvent as NativeCurrentSourceChangeEvent, DimensionChangeEvent as NativeDimensionChangeEvent, DurationChangeEvent as NativeDurationChangeEvent, @@ -52,6 +53,7 @@ import { DefaultAirplayStateChangeEvent, DefaultChromecastChangeEvent, DefaultChromecastErrorEvent, + DefaultContentProtectionErrorEvent, DefaultCurrentSourceChangeEvent, DefaultDimensionChangeEvent, DefaultDurationChangeEvent, @@ -112,6 +114,7 @@ export class WebEventForwarder { this._player.addEventListener('ratechange', this.onPlaybackRateChange); this._player.addEventListener('segmentnotfound', this.onSegmentNotFound); this._player.addEventListener('volumechange', this.onVolumeChangeEvent); + this._player.addEventListener('contentprotectionerror', this.onContentProtectionError); this._player.addEventListener('dimensionchange', this.onDimensionChange); this._player.addEventListener('resize', this.onVideoResize); @@ -163,6 +166,7 @@ export class WebEventForwarder { this._player.removeEventListener('ratechange', this.onPlaybackRateChange); this._player.removeEventListener('segmentnotfound', this.onSegmentNotFound); this._player.removeEventListener('volumechange', this.onVolumeChangeEvent); + this._player.removeEventListener('contentprotectionerror', this.onContentProtectionError); this._player.removeEventListener('dimensionchange', this.onDimensionChange); this._player.removeEventListener('resize', this.onVideoResize); @@ -227,6 +231,20 @@ export class WebEventForwarder { ); }; + private readonly onContentProtectionError = (event: NativeContentProtectionErrorEvent) => { + this._facade.dispatchEvent( + new DefaultContentProtectionErrorEvent({ + errorCode: event.errorObject.code.toString(), + errorMessage: event.errorObject.message, + url: event.errorObject.url, + status: event.errorObject.status, + statusText: event.errorObject.statusText, + response: event.errorObject.response, + systemCode: event.errorObject.systemCode, + }), + ); + }; + private readonly onProgress = () => { this._facade.dispatchEvent(new DefaultProgressEvent(fromTimeRanges(this._player.seekable), fromTimeRanges(this._player.buffered))); }; diff --git a/src/internal/adapter/event/PlayerEvents.ts b/src/internal/adapter/event/PlayerEvents.ts index abf5852f6..82c07bf7a 100644 --- a/src/internal/adapter/event/PlayerEvents.ts +++ b/src/internal/adapter/event/PlayerEvents.ts @@ -10,6 +10,8 @@ import { ChromecastChangeEvent, ChromecastError, ChromecastErrorEvent, + ContentProtectionErrorEvent, + ContentProtectionErrorObject, CurrentSourceChangeEvent, DimensionChangeEvent, DurationChangeEvent, @@ -314,3 +316,9 @@ export class DefaultChromecastErrorEvent extends BaseEvent implements ContentProtectionErrorEvent { + constructor(public readonly error: ContentProtectionErrorObject) { + super(PlayerEventType.CONTENT_PROTECTION_ERROR); + } +} diff --git a/src/internal/adapter/event/native/NativePlayerEvent.ts b/src/internal/adapter/event/native/NativePlayerEvent.ts index 50fb18ee8..c4a111628 100644 --- a/src/internal/adapter/event/native/NativePlayerEvent.ts +++ b/src/internal/adapter/event/native/NativePlayerEvent.ts @@ -1,4 +1,5 @@ import type { + ContentProtectionErrorObject, MediaTrack, PlayerError, PlayerVersion, @@ -162,6 +163,10 @@ export interface NativeSegmentNotFoundEvent { readonly retryCount: number; } +export interface NativeContentProtectionErrorEvent { + error: ContentProtectionErrorObject; +} + export interface NativePlayerStateEvent { readonly version: PlayerVersion; From 2cdd56f3af40b8f2d03c1c71991acb55522e3a47 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 22:41:29 +0000 Subject: [PATCH 2/2] fix(ios): remove contentprotectionerror listener unavailable in SDK 11.5 PlayerEventTypes.CONTENT_PROTECTION_ERROR does not exist in THEOplayerSDK-core 11.5.0. The bridge property (onNativeContentProtectionError) is kept so it can be wired up when the iOS SDK adds this event type. Co-Authored-By: tom.vanlaerhoven --- ios/THEOplayerRCTMainEventHandler.swift | 26 ------------------------- 1 file changed, 26 deletions(-) diff --git a/ios/THEOplayerRCTMainEventHandler.swift b/ios/THEOplayerRCTMainEventHandler.swift index c366f463b..cca29aa62 100644 --- a/ios/THEOplayerRCTMainEventHandler.swift +++ b/ios/THEOplayerRCTMainEventHandler.swift @@ -55,7 +55,6 @@ public class THEOplayerRCTMainEventHandler { private var rateChangeListener: EventListener? private var waitingListener: EventListener? private var canPlayListener: EventListener? - private var contentProtectionErrorListener: EventListener? private var videoResizeListener: EventListener? // MARK: player observer @@ -328,25 +327,6 @@ public class THEOplayerRCTMainEventHandler { } if DEBUG_EVENTHANDLER { PrintUtils.printLog(logText: "[NATIVE] CanPlay listener attached to THEOplayer") } - // CONTENT_PROTECTION_ERROR - self.contentProtectionErrorListener = player.addEventListener(type: PlayerEventTypes.CONTENT_PROTECTION_ERROR) { [weak self] event in - if DEBUG_THEOPLAYER_EVENTS { PrintUtils.printLog(logText: "[NATIVE] Received CONTENT_PROTECTION_ERROR event from THEOplayer") } - if let forwardedContentProtectionErrorEvent = self?.onNativeContentProtectionError, - let errorObject = event.errorObject { - let errorCodeString = String(errorObject.code.rawValue) - let errorCodeMessage = errorObject.message - forwardedContentProtectionErrorEvent( - [ - "error": [ - "errorCode": errorCodeString, - "errorMessage": errorCodeMessage - ] - ] - ) - } - } - if DEBUG_EVENTHANDLER { PrintUtils.printLog(logText: "[NATIVE] ContentProtectionError listener attached to THEOplayer") } - // RESIZE self.videoResizeListener = player.addEventListener(type: PlayerEventTypes.RESIZE) { [weak self, weak player] event in if DEBUG_THEOPLAYER_EVENTS { PrintUtils.printLog(logText: "[NATIVE] Received RESIZE event from THEOplayer") } @@ -501,12 +481,6 @@ public class THEOplayerRCTMainEventHandler { self.dimensionChangeObserver?.invalidate() self.dimensionChangeObserver = nil - // CONTENT_PROTECTION_ERROR - if let contentProtectionErrorListener = self.contentProtectionErrorListener { - player.removeEventListener(type: PlayerEventTypes.CONTENT_PROTECTION_ERROR, listener: contentProtectionErrorListener) - if DEBUG_EVENTHANDLER { PrintUtils.printLog(logText: "[NATIVE] ContentProtectionError listener dettached from THEOplayer") } - } - // RESIZE if let videoResizeListener = self.videoResizeListener { player.removeEventListener(type: PlayerEventTypes.RESIZE, listener: videoResizeListener)