Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions android/src/main/java/com/theoplayer/PlayerEventEmitter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -132,6 +133,7 @@ class PlayerEventEmitter internal constructor(
EVENT_THEOADS_EVENT,
EVENT_PRESENTATIONMODECHANGE,
EVENT_VOLUMECHANGE,
EVENT_CONTENTPROTECTIONERROR,
EVENT_DIMENSIONCHANGE,
EVENT_VIDEORESIZE
)
Expand Down Expand Up @@ -171,6 +173,7 @@ class PlayerEventEmitter internal constructor(
EVENT_THEOADS_EVENT,
EVENT_PRESENTATIONMODECHANGE,
EVENT_VOLUMECHANGE,
EVENT_CONTENTPROTECTIONERROR,
EVENT_DIMENSIONCHANGE,
EVENT_VIDEORESIZE
)
Expand Down Expand Up @@ -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] =
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions ios/THEOplayerRCTBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions ios/THEOplayerRCTMainEventHandler.swift

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 iOS volumeChangeListener is never detached (pre-existing)

The volumeChangeListener is declared at ios/THEOplayerRCTMainEventHandler.swift:45 and attached at line 162, but dettachListeners() (lines 380-515) never removes it — unlike every other listener in the file (play, pause, error, etc.). This is a pre-existing resource leak, not introduced by this PR, but the new contentProtectionErrorListener was correctly added to both attach and detach methods, so the PR author clearly followed the pattern. Worth fixing separately.

(Refers to line 45)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — confirmed this is pre-existing and out of scope for this PR. The volumeChangeListener is indeed attached but never removed in dettachListeners(). Worth fixing in a separate PR.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find. This is now fixed in this PR

Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class THEOplayerRCTMainEventHandler {
var onNativeRateChange: RCTDirectEventBlock?
var onNativeWaiting: RCTDirectEventBlock?
var onNativeCanPlay: RCTDirectEventBlock?
var onNativeContentProtectionError: RCTDirectEventBlock?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Content protection error event never fires on iOS because the native SDK listener is never attached

The native SDK listener for content protection errors is never registered (attachListeners() at ios/THEOplayerRCTMainEventHandler.swift:82-358) despite all bridge wiring being in place, so iOS users will never receive this event.

Impact: The contentprotectionerror event will silently never fire on iOS, even though the changelog claims iOS support.

Missing listener variable, attachment, and detachment in THEOplayerRCTMainEventHandler

Every other event in this handler follows a three-part pattern:

  1. A listener variable (e.g., private var errorListener: EventListener? at line 52)
  2. An attachment in attachListeners() (e.g., self.errorListener = player.addEventListener(type: PlayerEventTypes.ERROR) { ... } at line 263)
  3. A detachment in dettachListeners() (e.g., player.removeEventListener(type: PlayerEventTypes.ERROR, listener: errorListener) at line 444)

For contentProtectionError, all three are missing:

  • No private var contentProtectionErrorListener: EventListener? variable
  • No player.addEventListener(type: PlayerEventTypes.CONTENT_PROTECTION_ERROR) call in attachListeners()
  • No corresponding removeEventListener in dettachListeners()

The bridge property is correctly exported (ios/THEOplayerRCTBridge.m:50), the setter is wired (ios/THEOplayerRCTView.swift:473-477), and the event block property exists (ios/THEOplayerRCTMainEventHandler.swift:33), but without the listener, the THEOplayer iOS SDK event is never captured.

Prompt for agents
The iOS THEOplayerRCTMainEventHandler needs three additions to actually listen for the CONTENT_PROTECTION_ERROR event from the THEOplayer SDK:

1. Add a listener variable in the MARK: player Listeners section (around line 58, after videoResizeListener):
   private var contentProtectionErrorListener: EventListener?

2. Add listener attachment in attachListeners() (after canPlay attachment around line 328), following the same pattern as other listeners:
   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 forwardedEvent = self?.onNativeContentProtectionError {
           forwardedEvent(["error": ["errorCode": String(event.errorObject.code.rawValue), "errorMessage": event.errorObject.message]])
       }
   }

3. Add listener detachment in dettachListeners() (after canPlay detachment around line 476):
   if let contentProtectionErrorListener = self.contentProtectionErrorListener {
       player.removeEventListener(type: PlayerEventTypes.CONTENT_PROTECTION_ERROR, listener: contentProtectionErrorListener)
   }

Note: The exact event type name and errorObject properties should match the THEOplayer iOS SDK API. Check the SDK documentation for the correct PlayerEventTypes constant and error property access pattern.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional. The initial commit did include the full three-part listener pattern (contentProtectionErrorListener variable + attachListeners + dettachListeners), but the iOS CI build failed with:

error: type 'PlayerEventTypes' has no member 'CONTENT_PROTECTION_ERROR'

THEOplayerSDK-core 11.5.0 (installed by CI) does not expose PlayerEventTypes.CONTENT_PROTECTION_ERROR. The listener was removed in commit 2cdd56f to unblock the build.

The bridge wiring is kept in place so the listener can be re-added when the iOS SDK exposes this event type in a future release.

var onNativeDimensionChange: RCTDirectEventBlock?
var onNativeVideoResize: RCTDirectEventBlock?

Expand Down
6 changes: 6 additions & 0 deletions ios/THEOplayerRCTView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 58 additions & 0 deletions src/api/error/ContentProtectionError.ts
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions src/api/error/barrel.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './PlayerError';
export * from './ChromecastError';
export * from './ContentProtectionError';
17 changes: 16 additions & 1 deletion src/api/event/PlayerEvent.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -297,3 +297,18 @@ export interface SeekedEvent extends Event<PlayerEventType.SEEKED> {
*/
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<PlayerEventType.CONTENT_PROTECTION_ERROR> {
/**
* An error object containing additional information about the content protection error.
*/
readonly error: ContentProtectionErrorObject;
}
7 changes: 7 additions & 0 deletions src/api/player/PlayerEventMap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Event } from '../event/Event';
import type {
ContentProtectionErrorEvent,
CurrentSourceChangeEvent,
DimensionChangeEvent,
DurationChangeEvent,
Expand Down Expand Up @@ -63,6 +64,7 @@ export enum PlayerEventType {
DESTROY = 'destroy',
DIMENSION_CHANGE = 'dimensionchange',
VIDEO_RESIZE = 'videoresize',
CONTENT_PROTECTION_ERROR = 'contentprotectionerror',
}

/**
Expand Down Expand Up @@ -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;
}
8 changes: 8 additions & 0 deletions src/internal/THEOplayerView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
DefaultAirplayStateChangeEvent,
DefaultChromecastChangeEvent,
DefaultChromecastErrorEvent,
DefaultContentProtectionErrorEvent,
DefaultCurrentSourceChangeEvent,
DefaultDimensionChangeEvent,
DefaultDurationChangeEvent,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -116,6 +118,7 @@ interface THEOplayerRCTViewProps {
onNativeCastEvent: (event: NativeSyntheticEvent<NativeCastEvent>) => void;
onNativePresentationModeChange: (event: NativeSyntheticEvent<NativePresentationModeChangeEvent>) => void;
onNativeDeviceOrientationChanged: () => void;
onNativeContentProtectionError: (event: NativeSyntheticEvent<NativeContentProtectionErrorEvent>) => void;
onNativeDimensionChange: (event: NativeSyntheticEvent<NativeDimensionChangeEvent>) => void;
onNativeVideoResize: (event: NativeSyntheticEvent<NativeVideoResizeEvent>) => void;
}
Expand Down Expand Up @@ -422,6 +425,10 @@ export class THEOplayerView extends PureComponent<React.PropsWithChildren<THEOpl
this._facade?.dispatchEvent(new DefaultDimensionChangeEvent(width, height));
};

private _onContentProtectionError = (event: NativeSyntheticEvent<NativeContentProtectionErrorEvent>) => {
this._facade?.dispatchEvent(new DefaultContentProtectionErrorEvent(event.nativeEvent.error));
};

private _onVideoResize = (event: NativeSyntheticEvent<NativeVideoResizeEvent>) => {
this._facade?.dispatchEvent(new DefaultVideoResizeEvent(event.nativeEvent.videoWidth, event.nativeEvent.videoHeight));
};
Expand Down Expand Up @@ -503,6 +510,7 @@ export class THEOplayerView extends PureComponent<React.PropsWithChildren<THEOpl
onNativeCastEvent={this._onCastEvent}
onNativePresentationModeChange={this._onPresentationModeChange}
onNativeDeviceOrientationChanged={this._onDeviceOrientationChanged}
onNativeContentProtectionError={this._onContentProtectionError}
onNativeDimensionChange={this._onDimensionChange}
onNativeVideoResize={this._onVideoResize}
/>
Expand Down
18 changes: 18 additions & 0 deletions src/internal/adapter/WebEventForwarder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
CastStateChangeEvent,
ChromecastErrorEvent,
ChromelessPlayer,
ContentProtectionErrorEvent as NativeContentProtectionErrorEvent,
CurrentSourceChangeEvent as NativeCurrentSourceChangeEvent,
DimensionChangeEvent as NativeDimensionChangeEvent,
DurationChangeEvent as NativeDurationChangeEvent,
Expand Down Expand Up @@ -52,6 +53,7 @@ import {
DefaultAirplayStateChangeEvent,
DefaultChromecastChangeEvent,
DefaultChromecastErrorEvent,
DefaultContentProtectionErrorEvent,
DefaultCurrentSourceChangeEvent,
DefaultDimensionChangeEvent,
DefaultDurationChangeEvent,
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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)));
};
Expand Down
8 changes: 8 additions & 0 deletions src/internal/adapter/event/PlayerEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
ChromecastChangeEvent,
ChromecastError,
ChromecastErrorEvent,
ContentProtectionErrorEvent,
ContentProtectionErrorObject,
CurrentSourceChangeEvent,
DimensionChangeEvent,
DurationChangeEvent,
Expand Down Expand Up @@ -314,3 +316,9 @@ export class DefaultChromecastErrorEvent extends BaseEvent<PlayerEventType.CAST_
this.subType = CastEventType.CHROMECAST_ERROR;
}
}

export class DefaultContentProtectionErrorEvent extends BaseEvent<PlayerEventType.CONTENT_PROTECTION_ERROR> implements ContentProtectionErrorEvent {
constructor(public readonly error: ContentProtectionErrorObject) {
super(PlayerEventType.CONTENT_PROTECTION_ERROR);
}
}
5 changes: 5 additions & 0 deletions src/internal/adapter/event/native/NativePlayerEvent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
ContentProtectionErrorObject,
MediaTrack,
PlayerError,
PlayerVersion,
Expand Down Expand Up @@ -162,6 +163,10 @@ export interface NativeSegmentNotFoundEvent {
readonly retryCount: number;
}

export interface NativeContentProtectionErrorEvent {
error: ContentProtectionErrorObject;
}

export interface NativePlayerStateEvent {
readonly version: PlayerVersion;

Expand Down
Loading