Skip to main content
This guide walks you through creating custom plugins for React Native Video. You’ll learn how to implement plugins on both Android and iOS, handle lifecycle events, modify sources, and integrate with the plugin system.

Prerequisites

Before creating a plugin, ensure you’re familiar with:
  • React Native development and native modules
  • Android development (Kotlin, ExoPlayer)
  • iOS development (Swift, AVFoundation)
  • React Native Video’s plugin architecture
Plugins are native code extensions. You’ll need to write Kotlin for Android and Swift for iOS.

Plugin Development Options

The easiest way to create a plugin is to extend the base class: Advantages:
  • Auto-registration on instantiation
  • Default implementations for all methods
  • Only override what you need
  • Auto-cleanup on iOS (via deinit)
Use when: Building a standard plugin with typical lifecycle needs

Option 2: Implement ReactNativeVideoPluginSpec

Implement the interface directly for full control: Advantages:
  • Complete control over behavior
  • No base class overhead
  • Custom registration logic
Use when: You need fine-grained control or have specific architecture requirements

Creating Your First Plugin

Let’s build an Analytics Plugin that tracks playback events and modifies video sources.

Android Implementation

1

Create the plugin class

Create a new Kotlin file in your Android module:
android/src/main/java/com/yourapp/AnalyticsPlugin.kt
package com.yourapp

import com.twg.video.core.plugins.ReactNativeVideoPlugin
import com.twg.video.core.plugins.NativeVideoPlayerSource
import com.twg.video.core.player.NativeVideoPlayer
import java.lang.ref.WeakReference
import android.util.Log

class AnalyticsPlugin : ReactNativeVideoPlugin("Analytics") {
    private val TAG = "AnalyticsPlugin"
    private val sessionId = generateSessionId()
    
    override fun onPlayerCreated(player: WeakReference<NativeVideoPlayer>) {
        player.get()?.let { p ->
            Log.d(TAG, "Player created for URI: ${p.source.uri}")
            trackEvent("player_created", mapOf(
                "uri" to p.source.uri,
                "session_id" to sessionId
            ))
        }
    }
    
    override fun onPlayerDestroyed(player: WeakReference<NativeVideoPlayer>) {
        Log.d(TAG, "Player destroyed")
        trackEvent("player_destroyed", mapOf(
            "session_id" to sessionId
        ))
    }
    
    override fun overrideSource(source: NativeVideoPlayerSource): NativeVideoPlayerSource {
        // Add analytics headers to all requests
        source.headers["X-Session-Id"] = sessionId
        source.headers["X-App-Version"] = getAppVersion()
        source.headers["X-User-Id"] = getUserId()
        
        Log.d(TAG, "Modified source with analytics headers")
        return source
    }
    
    private fun generateSessionId(): String {
        return java.util.UUID.randomUUID().toString()
    }
    
    private fun getAppVersion(): String {
        // Implement version retrieval
        return "1.0.0"
    }
    
    private fun getUserId(): String {
        // Implement user ID retrieval
        return "user-123"
    }
    
    private fun trackEvent(eventName: String, properties: Map<String, String>) {
        // Send to your analytics service
        Log.d(TAG, "Event: $eventName, Properties: $properties")
    }
}
2

Register the plugin

Instantiate your plugin in your Application or Activity:
MainApplication.kt
class MainApplication : Application() {
    private lateinit var analyticsPlugin: AnalyticsPlugin
    
    override fun onCreate() {
        super.onCreate()
        
        // Plugin auto-registers on instantiation
        analyticsPlugin = AnalyticsPlugin()
    }
    
    override fun onTerminate() {
        // Clean up (important!)
        PluginsRegistry.shared.unregister(analyticsPlugin)
        super.onTerminate()
    }
}

iOS Implementation

1

Create the plugin class

Create a new Swift file in your iOS module:
ios/AnalyticsPlugin.swift
import Foundation
import ReactNativeVideo

class AnalyticsPlugin: ReactNativeVideoPlugin {
    private let sessionId: String
    
    init() {
        self.sessionId = UUID().uuidString
        super.init(name: "Analytics")
    }
    
    override func onPlayerCreated(player: Weak<NativeVideoPlayer>) {
        guard let p = player.value else { return }
        
        print("Player created for URI: \(p.source.uri)")
        trackEvent("player_created", properties: [
            "uri": p.source.uri,
            "session_id": sessionId
        ])
    }
    
    override func onPlayerDestroyed(player: Weak<NativeVideoPlayer>) {
        print("Player destroyed")
        trackEvent("player_destroyed", properties: [
            "session_id": sessionId
        ])
    }
    
    override func overrideSource(source: NativeVideoPlayerSource) async -> NativeVideoPlayerSource {
        // Add analytics headers
        var modifiedSource = source
        modifiedSource.headers["X-Session-Id"] = sessionId
        modifiedSource.headers["X-App-Version"] = getAppVersion()
        modifiedSource.headers["X-User-Id"] = await getUserId()
        
        print("Modified source with analytics headers")
        return modifiedSource
    }
    
    deinit {
        // Plugin auto-unregisters via base class deinit
        print("AnalyticsPlugin deallocated")
    }
    
    // MARK: - Private helpers
    
    private func getAppVersion() -> String {
        return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
    }
    
    private func getUserId() async -> String {
        // Implement user ID retrieval
        return "user-123"
    }
    
    private func trackEvent(_ eventName: String, properties: [String: String]) {
        // Send to your analytics service
        print("Event: \(eventName), Properties: \(properties)")
    }
}
2

Register the plugin

Instantiate your plugin in your AppDelegate:
AppDelegate.swift
import UIKit
import ReactNativeVideo

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var analyticsPlugin: AnalyticsPlugin?
    
    func application(_ application: UIApplication, 
                    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Plugin auto-registers on instantiation
        analyticsPlugin = AnalyticsPlugin()
        
        return true
    }
    
    // Plugin auto-unregisters when deallocated
}

Advanced Plugin Examples

CDN Switching Plugin

Automatically route videos through optimal CDN endpoints:
class CDNPlugin : ReactNativeVideoPlugin("CDN") {
    private val cdnMapping = mapOf(
        "storage.googleapis.com" to "cdn.fastly.net",
        "s3.amazonaws.com" to "cloudfront.net"
    )
    
    override fun overrideSource(source: NativeVideoPlayerSource): NativeVideoPlayerSource {
        val originalUri = android.net.Uri.parse(source.uri)
        val host = originalUri.host
        
        // Check if we have a CDN mapping
        cdnMapping[host]?.let { cdnHost ->
            val newUri = originalUri.buildUpon()
                .authority(cdnHost)
                .build()
                
            source.uri = newUri.toString()
            Log.d("CDNPlugin", "Switched to CDN: $host -> $cdnHost")
        }
        
        return source
    }
}

Authentication Plugin

Add dynamic bearer tokens to video requests:
class AuthPlugin : ReactNativeVideoPlugin("Auth") {
    private var accessToken: String? = null
    
    fun setAccessToken(token: String) {
        accessToken = token
    }
    
    override fun overrideSource(source: NativeVideoPlayerSource): NativeVideoPlayerSource {
        accessToken?.let { token ->
            source.headers["Authorization"] = "Bearer $token"
            Log.d("AuthPlugin", "Added auth header")
        } ?: run {
            Log.w("AuthPlugin", "No access token available")
        }
        
        return source
    }
}

// Usage:
// authPlugin.setAccessToken("eyJhbGc...")

Quality Control Plugin (Android)

Force specific bitrate or resolution:
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.common.TrackSelectionParameters

class QualityPlugin : ReactNativeVideoPlugin("Quality") {
    private val maxBitrate = 2_000_000 // 2 Mbps
    private val maxVideoWidth = 1920
    private val maxVideoHeight = 1080
    
    override fun onPlayerCreated(player: WeakReference<NativeVideoPlayer>) {
        player.get()?.exoPlayer?.let { exoPlayer ->
            val trackSelector = exoPlayer.trackSelector as? DefaultTrackSelector
            
            trackSelector?.parameters = trackSelector?.buildUponParameters()
                ?.setMaxVideoBitrate(maxBitrate)
                ?.setMaxVideoSize(maxVideoWidth, maxVideoHeight)
                ?.build() ?: return
                
            Log.d("QualityPlugin", "Applied quality constraints")
        }
    }
}

Custom Cache Plugin (Android)

Implement custom caching logic:
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.SimpleCache

class CachePlugin(private val cache: SimpleCache) : ReactNativeVideoPlugin("Cache") {
    
    override fun getMediaDataSourceFactory(
        source: NativeVideoPlayerSource,
        mediaDataSourceFactory: DataSource.Factory
    ): DataSource.Factory {
        // Only cache VOD content, not live streams
        if (source.uri.contains(".m3u8") && source.uri.contains("live")) {
            return mediaDataSourceFactory
        }
        
        // Wrap with cache data source
        return CacheDataSource.Factory()
            .setCache(cache)
            .setUpstreamDataSourceFactory(mediaDataSourceFactory)
            .setCacheWriteDataSinkFactory(null) // Read-only mode
    }
    
    override fun shouldDisableCache(source: NativeVideoPlayerSource): Boolean {
        // Disable default cache for live content
        return source.uri.contains("live")
    }
}

Exposing Plugins to JavaScript

To control plugins from JavaScript, create a native module bridge:
1

Define the interface

src/AnalyticsPluginManager.nitro.ts
import type { HybridObject } from 'react-native-nitro-modules';

export interface AnalyticsPluginManager
  extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> {
  enable(): void;
  disable(): void;
  setUserId(userId: string): void;
  trackEvent(eventName: string, properties: Record<string, string>): void;
  readonly isEnabled: boolean;
}
2

Implement on Android

android/src/main/java/com/yourapp/AnalyticsPluginManager.kt
class AnalyticsPluginManager : HybridAnalyticsPluginManagerSpec() {
    private var plugin: AnalyticsPlugin? = null
    
    override fun enable() {
        if (plugin == null) {
            plugin = AnalyticsPlugin()
        }
    }
    
    override fun disable() {
        plugin?.let {
            PluginsRegistry.shared.unregister(it)
            plugin = null
        }
    }
    
    override fun setUserId(userId: String) {
        plugin?.setUserId(userId)
    }
    
    override fun trackEvent(eventName: String, properties: ReadableMap) {
        plugin?.trackCustomEvent(eventName, properties.toHashMap())
    }
    
    override val isEnabled: Boolean
        get() = plugin != null
}
3

Implement on iOS

ios/AnalyticsPluginManager.swift
class AnalyticsPluginManager: HybridAnalyticsPluginManagerSpec {
    private var plugin: AnalyticsPlugin?
    
    func enable() {
        if plugin == nil {
            plugin = AnalyticsPlugin()
        }
    }
    
    func disable() {
        if let plugin = plugin {
            PluginsRegistry.shared.unregister(plugin: plugin)
            self.plugin = nil
        }
    }
    
    func setUserId(_ userId: String) {
        plugin?.setUserId(userId)
    }
    
    func trackEvent(_ eventName: String, properties: [String: String]) {
        plugin?.trackCustomEvent(eventName, properties: properties)
    }
    
    var isEnabled: Bool {
        return plugin != nil
    }
}
4

Use from JavaScript

src/index.tsx
import { NitroModules } from 'react-native-nitro-modules';
import type { AnalyticsPluginManager } from './AnalyticsPluginManager.nitro';

const manager = NitroModules.createHybridObject<AnalyticsPluginManager>('AnalyticsPluginManager');

export function enableAnalytics() {
  manager.enable();
}

export function disableAnalytics() {
  manager.disable();
}

export function setUserId(userId: string) {
  manager.setUserId(userId);
}

export function trackEvent(name: string, properties: Record<string, string>) {
  manager.trackEvent(name, properties);
}

export const isAnalyticsEnabled = manager.isEnabled;

Plugin Interface Reference

Implement these methods in your custom plugin:

Lifecycle Methods

// Android
override fun onPlayerCreated(player: WeakReference<NativeVideoPlayer>)
override fun onPlayerDestroyed(player: WeakReference<NativeVideoPlayer>)
override fun onVideoViewCreated(view: WeakReference<VideoView>)
override fun onVideoViewDestroyed(view: WeakReference<VideoView>)
// iOS
override func onPlayerCreated(player: Weak<NativeVideoPlayer>)
override func onPlayerDestroyed(player: Weak<NativeVideoPlayer>)
override func onVideoViewCreated(view: Weak<VideoComponentView>)
override func onVideoViewDestroyed(view: Weak<VideoComponentView>)

Source Modification

// Android
override fun overrideSource(source: NativeVideoPlayerSource): NativeVideoPlayerSource
// iOS (async!)
override func overrideSource(source: NativeVideoPlayerSource) async -> NativeVideoPlayerSource

DRM Support

// Android
override fun getDRMManager(source: NativeVideoPlayerSource): DRMManagerSpec?
// iOS
override func getDRMManager(source: NativeVideoPlayerSource) async -> (any DRMManagerSpec)?

Android-Only Methods

override fun getMediaDataSourceFactory(
    source: NativeVideoPlayerSource,
    mediaDataSourceFactory: DataSource.Factory
): DataSource.Factory?

override fun getMediaSourceFactory(
    source: NativeVideoPlayerSource,
    mediaSourceFactory: MediaSource.Factory,
    mediaDataSourceFactory: DataSource.Factory
): MediaSource.Factory?

override fun getMediaItemBuilder(
    source: NativeVideoPlayerSource,
    mediaItemBuilder: MediaItem.Builder
): MediaItem.Builder?

override fun shouldDisableCache(source: NativeVideoPlayerSource): Boolean

Best Practices

Player and view references are weak to prevent memory leaks. Always verify they’re not null:
// Android - use let
player.get()?.let { p ->
    // Safe to use 'p'
}

// Android - or check explicitly
val p = player.get()
if (p != null) {
    // Safe to use 'p'
}
// iOS - use guard
guard let p = player.value else { return }
// Safe to use 'p'

// iOS - or if let
if let p = player.value {
    // Safe to use 'p'
}
If your plugin doesn’t handle a particular feature, return null:
override fun getDRMManager(source: NativeVideoPlayerSource): DRMManagerSpec? {
    // Only handle custom DRM
    if (source.drmType == "custom") {
        return CustomDRMManager(source)
    }
    return null // Let other plugins handle it
}
Plugin methods are called in the playback path. Avoid heavy operations:
// BAD - blocks playback
override fun overrideSource(source: NativeVideoPlayerSource): NativeVideoPlayerSource {
    val token = fetchTokenFromServer() // Slow network call!
    source.headers["Authorization"] = token
    return source
}

// GOOD - use cached values
override fun overrideSource(source: NativeVideoPlayerSource): NativeVideoPlayerSource {
    val token = cachedToken ?: "" // Fast lookup
    source.headers["Authorization"] = token
    return source
}
Always unregister plugins and release resources:
// Android - manual cleanup
override fun onDestroy() {
    PluginsRegistry.shared.unregister(plugin)
    super.onDestroy()
}
// iOS - automatic via deinit
deinit {
    // Base class handles unregistration
    // Clean up your custom resources here
    cancellables.forEach { $0.cancel() }
}
Don’t crash the video player on errors:
override fun overrideSource(source: NativeVideoPlayerSource): NativeVideoPlayerSource {
    try {
        // Risky operation
        val modifiedUrl = transformUrl(source.uri)
        source.uri = modifiedUrl
    } catch (e: Exception) {
        Log.e("MyPlugin", "Failed to transform URL", e)
        // Return original source on error
    }
    return source
}
iOS plugin methods support async/await. Use it for I/O operations:
override func overrideSource(source: NativeVideoPlayerSource) async -> NativeVideoPlayerSource {
    var modified = source
    
    // Async token fetch is OK on iOS
    if let token = await fetchToken() {
        modified.headers["Authorization"] = "Bearer \(token)"
    }
    
    return modified
}

Testing Your Plugin

Unit Testing

import org.junit.Test
import org.junit.Assert.*
import java.lang.ref.WeakReference

class AnalyticsPluginTest {
    @Test
    fun `overrideSource adds session header`() {
        val plugin = AnalyticsPlugin()
        val source = NativeVideoPlayerSource().apply {
            uri = "https://example.com/video.mp4"
        }
        
        val modified = plugin.overrideSource(source)
        
        assertNotNull(modified.headers["X-Session-Id"])
        assertTrue(modified.headers["X-Session-Id"]!!.isNotEmpty())
    }
    
    @Test
    fun `onPlayerCreated handles null player`() {
        val plugin = AnalyticsPlugin()
        val weakRef = WeakReference<NativeVideoPlayer>(null)
        
        // Should not crash
        plugin.onPlayerCreated(weakRef)
    }
}

Integration Testing

Test your plugin with real video playback:
import { enableAnalytics, setUserId } from './analytics-plugin';
import { useVideoPlayer, VideoView } from 'react-native-video';

// Enable plugin before tests
enableAnalytics();
setUserId('test-user-123');

function TestPlayer() {
  const player = useVideoPlayer({
    uri: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
  });
  
  return <VideoView player={player} style={{ width: 300, height: 200 }} />;
}

// Verify headers are added via network inspection or logging

Publishing Your Plugin

If you want to share your plugin with the community:
1

Create a package

Structure your plugin as a standalone npm package:
@your-org/react-native-video-analytics/
├── android/
│   └── src/main/java/...
├── ios/
│   └── AnalyticsPlugin.swift
├── src/
│   └── index.ts
├── package.json
└── README.md
2

Configure package.json

{
  "name": "@your-org/react-native-video-analytics",
  "version": "1.0.0",
  "description": "Analytics plugin for React Native Video",
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "peerDependencies": {
    "react-native": "*",
    "react-native-video": ">=7.0.0"
  }
}
3

Document usage

Create comprehensive README with:
  • Installation instructions
  • Setup guide for Android and iOS
  • API reference
  • Usage examples
  • Troubleshooting section
4

Publish to npm

npm publish --access public

Next Steps

Plugin Overview

Learn about plugin architecture

DRM Plugin

Study the official DRM plugin

API Reference

Full plugin interface docs

Resources