Skip to content

Commit

Permalink
Merge pull request #430 from THEOplayer/feature/omid
Browse files Browse the repository at this point in the history
[WIP] Feature/omid
  • Loading branch information
tvanlaerhoven authored Oct 30, 2024
2 parents 70b1721 + 70ff848 commit d18d7ed
Show file tree
Hide file tree
Showing 12 changed files with 241 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Fixed an issue on iOS where the dynamic island (iphone 14 plus and higher) briefly disappeared when updating the nowPlayingInfo on for example backgrounding the app.
- Fixed an issue on Android when using Expo, where the Expo plugin would not add THEOplayer's Maven repo to the project's repositories list.

### Added

- Added the `Omid` API for ads, which can be used to manage friendly video controls overlay obstructions.

## [8.6.0] - 24-10-25

### Added
Expand Down
52 changes: 52 additions & 0 deletions android/src/main/java/com/theoplayer/ads/AdsModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@
package com.theoplayer.ads

import android.util.Log
import android.view.View
import com.facebook.react.bridge.*
import com.theoplayer.source.SourceAdapter
import com.theoplayer.util.ViewResolver
import com.theoplayer.ReactTHEOplayerView
import com.theoplayer.android.api.ads.OmidFriendlyObstruction
import com.theoplayer.android.api.ads.OmidFriendlyObstructionPurpose
import com.theoplayer.android.api.error.THEOplayerException

private const val TAG = "THEORCTAdsModule"

private const val PROP_OMID_VIEW = "view"
private const val PROP_OMID_PURPOSE = "purpose"
private const val PROP_OMID_REASON = "reason"

class AdsModule(context: ReactApplicationContext) : ReactContextBaseJavaModule(context) {
private val sourceHelper = SourceAdapter()
private val viewResolver: ViewResolver = ViewResolver(context)
Expand Down Expand Up @@ -133,4 +140,49 @@ class AdsModule(context: ReactApplicationContext) : ReactContextBaseJavaModule(c
}
}
}

@ReactMethod
fun addFriendlyObstruction(tag: Int, obstruction: ReadableMap) {
val obsTag =
if (obstruction.hasKey(PROP_OMID_VIEW)) obstruction.getInt(PROP_OMID_VIEW) else null
val purpose = if (obstruction.hasKey(PROP_OMID_PURPOSE)) {
when (obstruction.getString(PROP_OMID_PURPOSE)) {
"videoControls" -> OmidFriendlyObstructionPurpose.VIDEO_CONTROLS
"closeAd" -> OmidFriendlyObstructionPurpose.CLOSE_AD
"notVisible" -> OmidFriendlyObstructionPurpose.NOT_VISIBLE
else -> OmidFriendlyObstructionPurpose.OTHER
}
} else null
val reason =
if (obstruction.hasKey(PROP_OMID_REASON)) obstruction.getString(PROP_OMID_REASON) else null

if (obsTag !== null) {
viewResolver.resolveViewByTag(obsTag) { obsView: View? ->
addFriendlyObstruction(tag, obsView, purpose, reason)
}
}
}

private fun addFriendlyObstruction(
tag: Int,
obsView: View?,
purpose: OmidFriendlyObstructionPurpose?,
reason: String?
) {
if (obsView == null || purpose == null) {
return
}
viewResolver.resolveViewByTag(tag) { view: ReactTHEOplayerView? ->
view?.player?.ads?.omid?.addFriendlyObstruction(
OmidFriendlyObstruction(obsView, purpose, reason)
)
}
}

@ReactMethod
fun removeAllFriendlyObstructions(tag: Int) {
viewResolver.resolveViewByTag(tag) { view: ReactTHEOplayerView? ->
view?.player?.ads?.omid?.removeAllFriendlyObstructions()
}
}
}
11 changes: 6 additions & 5 deletions android/src/main/java/com/theoplayer/util/ViewResolver.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
package com.theoplayer.util

import android.util.Log
import android.view.View
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.UIManagerModule
import com.theoplayer.ReactTHEOplayerView

private const val TAG = "ViewResolver"
private const val INVALID_TAG = -1

@Suppress("UNCHECKED_CAST")
class ViewResolver(private val reactContext: ReactApplicationContext) {
private var uiManager: UIManagerModule? = null

fun resolveViewByTag(tag: Int, onResolved: (view: ReactTHEOplayerView?) -> Unit) {
fun <T: View> resolveViewByTag(tag: Int, onResolved: (view: T?) -> Unit) {
if (tag == INVALID_TAG) {
// Don't bother trying to resolve an invalid tag.
onResolved(null)
Expand All @@ -21,10 +22,10 @@ class ViewResolver(private val reactContext: ReactApplicationContext) {
}
uiManager?.addUIBlock {
try {
onResolved(it.resolveView(tag) as ReactTHEOplayerView)
onResolved(it.resolveView(tag) as? T?)
} catch (e: Exception) {
// The ReactTHEOplayerView instance could not be resolved: log but do not forward exception.
Log.e(TAG, "Failed to resolve ReactTHEOplayerView tag $tag: $e")
// The View instance could not be resolved: log but do not forward exception.
Log.e(TAG, "Failed to resolve View tag $tag: $e")
onResolved(null)
}
}
Expand Down
5 changes: 5 additions & 0 deletions ios/THEOplayerRCTBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ @interface RCT_EXTERN_REMAP_MODULE(THEORCTAdsModule, THEOplayerRCTAdsAPI, NSObje
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(addFriendlyObstruction:(nonnull NSNumber *)node
obstruction:(NSDictionary)obstruction)

RCT_EXTERN_METHOD(removeAllFriendlyObstructions:(nonnull NSNumber *)node)

@end

// ----------------------------------------------------------------------------
Expand Down
16 changes: 16 additions & 0 deletions ios/THEOplayerRCTTypeUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,22 @@ class THEOplayerRCTTypeUtils {
return .moviePlayback
}
}

class func omidFriendlyObstructionPurposeFromString(_ purposeString: String) -> OmidFriendlyObstructionPurpose {
switch purposeString {
case "videoControls":
return OmidFriendlyObstructionPurpose.mediaControls
case "closeAd":
return OmidFriendlyObstructionPurpose.closeAd
case "notVisible":
return OmidFriendlyObstructionPurpose.notVisible
case "other":
return OmidFriendlyObstructionPurpose.other
default:
return OmidFriendlyObstructionPurpose.other
}

}

#if os(iOS)
class func cacheStatusToString(_ status: CacheStatus) -> String {
Expand Down
55 changes: 55 additions & 0 deletions ios/ads/THEOplayerRCTAdsAPI+Omid.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// THEOplayerRCTAdsAPI+Omid.swift

import Foundation
import THEOplayerSDK
import UIKit

let PROP_OMID_VIEW: String = "view"
let PROP_OMID_PURPOSE: String = "purpose"
let PROP_OMID_REASON: String = "reason"

extension THEOplayerRCTAdsAPI {

#if canImport(THEOplayerGoogleIMAIntegration)
@objc(addFriendlyObstruction:obstruction:)
func addFriendlyObstruction(_ node: NSNumber, obstruction: NSDictionary) {
DispatchQueue.main.async {
if let obstructionNode = obstruction[PROP_OMID_VIEW] as? NSNumber,
let purposeString = obstruction[PROP_OMID_PURPOSE] as? String,
let theView = self.bridge.uiManager.view(forReactTag: node) as? THEOplayerRCTView,
let obstructionView = self.bridge.uiManager.view(forReactTag: obstructionNode),
let ads = theView.ads() {
let obstruction = OmidFriendlyObstruction(view: obstructionView,
purpose: THEOplayerRCTTypeUtils.omidFriendlyObstructionPurposeFromString(purposeString),
detailedReason: obstruction[PROP_OMID_REASON] as? String)
ads.omid.addFriendlyObstruction(friendlyObstruction: obstruction)
}
}
}

@objc(removeAllFriendlyObstructions:)
func removeAllFriendlyObstructions(_ node: NSNumber) {
DispatchQueue.main.async {
if let theView = self.bridge.uiManager.view(forReactTag: node) as? THEOplayerRCTView,
let ads = theView.ads() {
ads.omid.removeFriendlyObstructions()
}
}
}

#else

@objc(addFriendlyObstruction:obstruction:)
func addFriendlyObstruction(_ node: NSNumber, obstruction: NSDictionary) {
if DEBUG_ADS_API { print(ERROR_MESSAGE_ADS_UNSUPPORTED_FEATURE) }
}

@objc(removeAllFriendlyObstructions:)
func removeAllFriendlyObstructions(_ node: NSNumber) {
if DEBUG_ADS_API { print(ERROR_MESSAGE_ADS_UNSUPPORTED_FEATURE) }
}

#endif
}


8 changes: 8 additions & 0 deletions src/api/ads/AdsAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { AdDescription } from "../source/ads/Ads";
import type { AdBreak } from "./AdBreak";
import type { Ad } from "./Ad";
import type { GoogleDAI } from "./GoogleDai";
import { Omid } from './Omid';

export interface AdsAPI {
/**
Expand Down Expand Up @@ -50,4 +51,11 @@ export interface AdsAPI {
* <br/> - Only available when the feature or extension `'google-dai'` is enabled.
*/
readonly dai?: GoogleDAI;

/**
* The Omid API, which can be used to add as well as remove friendly video controls overlay obstructions.
*
* @since React Native THEOplayer SDK v8.7.0.
*/
readonly omid?: Omid;
}
58 changes: 58 additions & 0 deletions src/api/ads/Omid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import { View } from 'react-native';

export enum OmidFriendlyObstructionPurpose {
/**
* The video overlay is one of the video player's control button.
*/
VIDEO_CONTROLS = 'videoControls',

/**
* The video overlay is for the purpose of closing the advertisement.
*/
CLOSE_AD = 'closeAd',

/**
* The video overlay is transparent and will not affect viewability.
*/
NOT_VISIBLE = 'notVisible',

/**
* The video overlay is meant for other purposes.
*/
OTHER = 'other',
}

export interface OmidFriendlyObstruction {
/**
* The View of the friendly obstruction.
*/
view: React.RefObject<View>;

/**
* The {@link OmidFriendlyObstructionPurpose} of the friendly obstruction.
*/
purpose: OmidFriendlyObstructionPurpose;

/**
* The optional reason for the friendly obstruction.
*/
reason?: string;
}

/**
* The Omid API, which can be used to add as well as remove friendly video controls overlay obstructions.
*/
export interface Omid {

/**
* Adds an {@link OmidFriendlyObstruction}.
* @param obstruction
*/
addFriendlyObstruction(obstruction: OmidFriendlyObstruction): void;

/**
* Removes all {@link OmidFriendlyObstruction}s.
*/
removeAllFriendlyObstructions(): void;
}
1 change: 1 addition & 0 deletions src/api/ads/barrel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './CompanionAd';
export * from './GoogleDai';
export * from './GoogleImaAd';
export * from './GoogleImaConfiguration';
export * from './Omid';
8 changes: 8 additions & 0 deletions src/internal/adapter/ads/THEOplayerNativeAdsAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { Ad, AdBreak, AdDescription, AdsAPI, GoogleDAI, THEOplayerView } from 'react-native-theoplayer';
import { NativeModules } from 'react-native';
import { THEOplayerNativeGoogleDAI } from './THEOplayerNativeGoogleDAI';
import { THEOplayerNativeOmid } from './THEOplayerNativeOmid';
import { Omid } from '../../../api/ads/Omid';

const NativeAdsModule = NativeModules.THEORCTAdsModule;

export class THEOplayerNativeAdsAdapter implements AdsAPI {
private readonly _dai: GoogleDAI;
private readonly _omid: Omid;

constructor(private _player: THEOplayerView) {
this._dai = new THEOplayerNativeGoogleDAI(this._player);
this._omid = new THEOplayerNativeOmid(this._player);
}

playing(): Promise<boolean> {
Expand Down Expand Up @@ -38,4 +42,8 @@ export class THEOplayerNativeAdsAdapter implements AdsAPI {
get dai(): GoogleDAI | undefined {
return this._dai;
}

get omid(): Omid | undefined {
return this._omid;
}
}
21 changes: 21 additions & 0 deletions src/internal/adapter/ads/THEOplayerNativeOmid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { THEOplayerView } from 'react-native-theoplayer';
import { findNodeHandle, NativeModules } from 'react-native';
import { Omid, OmidFriendlyObstruction } from '../../../api/ads/Omid';

const NativeAdsModule = NativeModules.THEORCTAdsModule;

export class THEOplayerNativeOmid implements Omid {
public constructor(private readonly _player: THEOplayerView) {}

addFriendlyObstruction(obstruction: OmidFriendlyObstruction): void {
NativeAdsModule.addFriendlyObstruction(this._player.nativeHandle, {
view: findNodeHandle(obstruction.view.current),
purpose: obstruction.purpose,
reason: obstruction.reason,
});
}

removeAllFriendlyObstructions(): void {
NativeAdsModule.removeAllFriendlyObstructions(this._player.nativeHandle);
}
}
7 changes: 7 additions & 0 deletions src/internal/adapter/ads/THEOplayerWebAdsAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Ad, AdBreak, AdDescription, AdsAPI, GoogleDAI } from 'react-native-theoplayer';
import { THEOplayerWebGoogleDAI } from './THEOplayerWebGoogleDAI';
import type { ChromelessPlayer } from 'theoplayer';
import { Omid } from '../../../api/ads/Omid';

export class THEOplayerWebAdsAdapter implements AdsAPI {
private readonly _player: ChromelessPlayer;
Expand Down Expand Up @@ -48,4 +49,10 @@ export class THEOplayerWebAdsAdapter implements AdsAPI {
}
return this._dai;
}

get omid(): Omid | undefined {
// NYI
console.warn('The Omid API is not yet implemented for Web platforms.');
return undefined;
}
}

0 comments on commit d18d7ed

Please sign in to comment.