166 Commits

Author SHA1 Message Date
1f513d4b2b Fix Railbird regressions found during v6.19.2 review
Some checks failed
Build Android / Build Android Example App (pull_request) Has been cancelled
Build Android / Build Android Example App With Ads (pull_request) Has been cancelled
Build iOS / Build iOS Example App (pull_request) Has been cancelled
Build iOS / Build iOS Example App With Ads (pull_request) Has been cancelled
Build iOS / Build iOS Example App With Caching (pull_request) Has been cancelled
Check Android / Kotlin-Lint (pull_request) Has been cancelled
Check iOS / Swift-Lint (pull_request) Has been cancelled
Check iOS / Swift-Format (pull_request) Has been cancelled
Check JS / Check TS (tsc) (pull_request) Has been cancelled
Check JS / Lint JS (eslint, prettier) (pull_request) Has been cancelled
2026-08-04 02:55:23 -07:00
5a1be8ba4c Merge upstream React Native Video v6.19.2 2026-08-04 01:30:58 -07:00
d7563cfd93 Fix video rendering with React Native 0.86
Fold the mobile repository's react-native-video patch into the maintained fork. Avoid the bridgeless RCTBridge.current() trap, preserve the TypeScript install build, and use the supported absolute-fill style so legacy-interoperability video views receive their layout.
2026-08-03 23:30:41 -07:00
69a9159a43 Load video duration asynchronously
Some checks failed
Build Android / Build Android Example App (pull_request) Has been cancelled
Build Android / Build Android Example App Without Ads (pull_request) Has been cancelled
Build iOS / Build iOS Example App (pull_request) Has been cancelled
Build iOS / Build iOS Example App With Ads (pull_request) Has been cancelled
Build iOS / Build iOS Example App With Caching (pull_request) Has been cancelled
Check Android / Kotlin-Lint (pull_request) Has been cancelled
Check CLang / CLang-Format (pull_request) Has been cancelled
Check iOS / Swift-Lint (pull_request) Has been cancelled
Check iOS / Swift-Format (pull_request) Has been cancelled
Check JS / Check TS (tsc) (pull_request) Has been cancelled
Check JS / Lint JS (eslint, prettier) (pull_request) Has been cancelled
Test Docs build / build-docs (pull_request) Has been cancelled
2026-07-23 13:20:28 -07:00
Kamil Moskała
062fcec7fe chore: release v6.19.2 2026-04-28 14:43:56 +02:00
Joar Karlsson
ae7ff4fa07 fix: replace absoluteFillObject with absoluteFill (#4880)
* Update for 0.85

* fix formatting

* Delete bun.lock

* chore: update deps

* chore: update `absoluteFillObject` in tv example

---------

Co-authored-by: Joar Karlsson <joar.karlsson@carnegie.se>
Co-authored-by: Kamil Moskała <91079590+moskalakamil@users.noreply.github.com>
Co-authored-by: Kamil Moskała <moskalakamil07@gmail.com>
2026-04-28 14:23:41 +02:00
Kamil Moskała
cd72fc1f28 chore: release v6.19.1 2026-03-15 13:25:49 +01:00
Kamil Moskała
11962d9e02 chore: swift lint (#4856) 2026-03-15 12:56:47 +01:00
Kamil Moskała
92b0a0e416 fix: RCTVideoManager crash in bridgeless mode RN0.84 (#4855) 2026-03-15 12:56:03 +01:00
Ibad9
fc936c49ef fix(ios): IMA ad container resizing on orientation change (#4771)
* Fix IMA ad container not resizing on orientation change (iOS)

* refactor: lint

* chore: change comment text

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* refactor: remove redundant frame initialization

---------

Co-authored-by: Ibad Ahmed <ibadahmed@Ibads-MacBook-Pro.local>
Co-authored-by: Kamil Moskała <91079590+moskalakamil@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-09 13:40:31 +01:00
c6bd3e3ee4 use object fill 2026-01-21 11:35:19 -08:00
Kamil
9c981f6f72 chore: release v6.19.0 2026-01-19 15:57:12 +01:00
Filip Wnęk
88ac1ae1dc feat!: add DAI support (#4816)
* feat: add DAI support

* refactor!: unify CSAI and DAI configuration into single ad prop

* fix(android): remove broken wait() in DAI initialization

* feat(android): support HLS/DASH format in DAI

* fix: lint formatting issues

* refactor(android): simplify URI parsing

* refactor(android): rename backupStreamUri to fallbackStreamUri for consistency

* refactor: restore deleted comment

* refactor: remove expo-dai example app

* feat: add streamType field

* refactor: simplify DAI config normalization

* refactor: rename type value from dai to ssai

* docs: replace DAI terminology with SSAI

* docs: change ssai desc

Co-authored-by: Kamil Moskała <91079590+moskalakamil@users.noreply.github.com>

* fix(ios): conditional background playback for DAI based on PiP settings

* fix: remove unused import

---------

Co-authored-by: Maciej Budzinski <maciej.budzinski.93@gmail.com>
Co-authored-by: Kamil Moskała <91079590+moskalakamil@users.noreply.github.com>
2026-01-15 21:47:14 +01:00
2d953c4c46 fix null-safety-checks and f bounded polymorphism 2025-11-26 13:17:41 -07:00
Harang
f387177785 fix(android): correct videoTrack type definitions to match Android implementation (#4778) 2025-11-23 13:47:56 +01:00
Kamil Moskała
9d3ca8fd55 chore: release v6.18.0 (#4777) 2025-11-18 15:23:20 +01:00
Kamil Moskała
ba65ab1233 fix: entering PiP mode when controls are true (#4776) 2025-11-18 14:45:40 +01:00
langemike
fbb260e916 fix: prevent audiovisualBackgroundPlaybackPolicy crash (#4763) 2025-11-07 16:40:30 +01:00
Krzysztof Moch
90ccf4be29 chore: lint code 2025-11-05 13:24:04 +01:00
Alex Shumihin
f2afd16d0b feat(ios): add PublicAudioSessionManager for audio session management (#4747) 2025-11-05 12:58:32 +01:00
Harang
05cd5972c2 fix(android): prevent duplicate onVideoEnd callback on prop changes (#4762) 2025-11-05 12:43:24 +01:00
Krzysztof Moch
a9f752435f fix(ci): update ios device for builds (#4757) 2025-11-03 14:46:19 +01:00
Jianlong-Nie
5d75b48295 fix(iOS): use top-most presented view controller for fullscreen presentation on iOS (#4753) 2025-11-02 11:38:01 +01:00
Filip Wnęk
b561eff5a5 docs: update Offline Video SDK links (#4749) 2025-10-30 21:14:19 +01:00
Filip Wnęk
4a2229639c docs(readme): add Background Upload SDK section (#4746) 2025-10-29 13:39:50 +01:00
Krzysztof Moch
92514e72a6 chore: release v6.17.0 2025-10-06 13:32:39 +02:00
Lethe
5074ca5a1b fix: android control option is not work when init video (#4698) 2025-09-19 13:19:24 +02:00
Seyed Mostafa Hasani
7d233f414f feat(android): enable flexible page sizes in native build configuration (#4691) 2025-09-13 13:13:04 +02:00
Sung Jeon
724f63930d feat(android): report full native stack trace on error (#4651) 2025-09-01 13:52:17 +02:00
Sung Jeon
4b996fc514 fix(player): trigger relayout after exiting pip (#4665) 2025-08-26 13:37:49 +02:00
Qusai Eilouti
d2c92a1f3f fix(ios): disable audio session management when no views (#4666) 2025-08-26 13:36:54 +02:00
arnav-cashify
5b59b06d1e fix(android): catch errors from activity.packageManager 2025-08-26 13:34:48 +02:00
Jakub Boguski
b11d59ba26 docs: Update copy around Offline Video SDK (#4646) 2025-08-12 10:23:23 +02:00
Kamil Moskała
60baecdf73 feat(examples): add offline-video-starter as submodule (#4644) 2025-08-11 14:04:40 +02:00
Krzysztof Moch
b69ede1a27 chore: remove old docs deployment (#4626) 2025-07-25 11:17:35 +02:00
Krzysztof Moch
31915e432b docs: Add llms.txt and llms-full.txt (#4603)
Co-authored-by: Pieczasz <bartekp854@gmail.com>
2025-07-13 18:48:54 +02:00
Dawid Wyrkowski
bf810386b9 fix(ios): add audiovisualBackgroundPlaybackPolicy (#4570) 2025-07-13 17:54:38 +02:00
Krzysztof Moch
e77a12ebaa chore: release v6.16.1 2025-07-08 16:12:07 +02:00
Bartłomiej Piekarz
481aa7ca65 fix(android)/aspect-ratio (#4594) 2025-07-08 15:17:43 +02:00
Krzysztof Moch
86ea0e3101 chore: release v6.16.0 2025-07-02 11:18:21 +02:00
Lukas
2d7e03942d fix(ios): allow audio mixing when none of the players are playing (#4579) 2025-07-02 10:40:17 +02:00
Bartłomiej Piekarz
978683b645 feat(android): replace custom VideoView with media3 PlayerView (#4581) 2025-07-02 08:46:19 +02:00
Krzysztof Moch
1a4e3b0637 chore: update README 2025-07-01 08:33:32 +02:00
Krzysztof Moch
18527fcc5c chore(infra): update validation script (#4586) 2025-06-30 13:21:35 +02:00
Tuan Luong
94967fc4a6 fix(android): create custom event class for dispatcher (#4575) 2025-06-23 09:40:14 +02:00
Mosab Anini
b56b647d85 fix(android): speed control is not applied (#4572) 2025-06-18 08:17:47 +02:00
Varuzh1
b51b579ff0 fix(ios): retain cycle and memory leak involving the RCTVideo instance when using Google IMA ads in react-native-video. (#4574)
Co-authored-by: Varuzhan Khachatryan <varuzhan.khachatryan23@gmail.com>
2025-06-18 08:12:21 +02:00
Krzysztof Moch
60c3a08ae3 chore: release v6.15.0 2025-06-12 13:44:37 +02:00
Krzysztof Moch
79d25bf1bd chore: fix repository url in package.json 2025-06-11 12:28:57 +02:00
Lukas
3576a134e6 feat: allow audio mixing if one of the video views require it (#4559) 2025-06-11 12:23:53 +02:00
Wojciech Ogrodowczyk
88c20d1c06 feat(ios): forward real fullscreen events from AVPlayer instead of guessing (#4509) 2025-06-11 09:33:38 +02:00
Kamil Moskała
45fd250adf chore(ios): make DRMParams properties public (#4568) 2025-06-11 09:32:58 +02:00
Krzysztof Moch
9f03cc5a09 fix(web): add missing component type 2025-06-11 08:36:37 +02:00
Kamil Moskała
9cf780276a feat(plugin): overrideMediaSourceFactory (#4566) 2025-06-05 12:36:55 +02:00
Krzysztof Moch
55449d9ec4 chore: release v6.14.1 2025-05-28 16:29:02 +02:00
Kamil Moskała
9449eb34f3 feat(android): allow plugins to override drm session manager (#4558) 2025-05-28 16:11:57 +02:00
Krzysztof Moch
e165cdefa1 docs: update readme (#4554) 2025-05-22 16:56:36 +02:00
Krzysztof Moch
6c5bb77def chore: release v6.14.0 2025-05-10 17:06:14 +02:00
Krzysztof Moch
701f777b6e chore: cleanup package.json (#4544) 2025-05-10 16:56:53 +02:00
Ben Winters
089e938aeb fix(ios): set _isBuffering = true only if newValue is also true (#4532) 2025-05-10 16:46:18 +02:00
Jonathan Jacobs
fc1e3f4fd1 feat(web): allow style prop overrides (#4528)
Co-authored-by: Jonathan Jacobs <jonathan@yoco.co.za>
2025-05-10 16:45:11 +02:00
jason
ecfe12aa81 fix(ios): default cropStart fallback (#4540) 2025-05-10 16:43:41 +02:00
BryanChemp
49173edd59 Changing switch sintaxe to preserve compatibility with java 11 or minus (#4521) 2025-05-07 14:27:34 +02:00
Krzysztof Moch
987be4b293 fix(ios): swfit modular headers (#4527) 2025-04-24 19:02:26 +02:00
Krzysztof Moch
1c95041a4a chore(example): bump react-native & expo version (#4500) 2025-04-23 12:42:36 +02:00
Kamil Moskała
368830954e docs: update links (#4525) 2025-04-20 22:24:00 +02:00
Kamil Moskała
dc4195e339 chore: add issue number to link (#4524) 2025-04-20 22:11:08 +02:00
Krzysztof Moch
735e35a2ea chore: release v6.13.0 2025-04-18 18:08:14 +02:00
Krzysztof Moch
b1b3db3010 feat(ios): add overridePlayerAsset to AVPlayerPlugin (#4522) 2025-04-18 18:02:55 +02:00
Kamil Moskała
2bdc0daf2e chore: update bot messages (#4517) 2025-04-16 09:30:52 +02:00
Ryan Fowler
b17c319c32 fix(macCatalyst): allowsVideoFrameAnalysis not available in catalyst until 18.0 (#4515) 2025-04-14 15:48:15 +02:00
Kamil Moskała
40340467d7 fix(tvos): build (#4511) 2025-04-14 15:47:15 +02:00
Krzysztof Moch
ab8ec0699b chore: release v6.12.0 2025-04-06 16:42:41 +02:00
Krzysztof Moch
8836362609 feat(ios): allow to disable audio sessions management (#4492)
* feat(ios): allow to disable audio sessions management

* chore: lint code

* chore: fix alphabetical order
2025-04-06 16:39:18 +02:00
Vijay Bindraban
a849cc19e8 fix: add extra checks to determine if a file is local (#4503)
* fix: beter isLocalAsset checks

* fix: add "asset://" to possible assets paths

* Update src/Video.tsx

---------

Co-authored-by: Olivier Bouillet <62574056+freeboub@users.noreply.github.com>
2025-04-06 16:18:34 +02:00
Kamil Moskała
41ddc5c27a feat(android): initial bitrate (#4480)
* feat(android): implement initialBitrate

* refactor: reorder type

* docs: add initialBitrate section

* fix: lint

* refactor: move `initialBitrate` into `source.bufferConfig`
2025-04-06 16:16:58 +02:00
Wojciech Ogrodowczyk
d2e5d9c64e feat(ios): set playback speed controls to initial playback rate (#4495)
Otherwise, the Video can be initiated with playback rate 0.5 and
play correctly (slower), but in native playback speed controls 1.0
will be marked as current playback speed.
2025-04-02 17:37:56 +02:00
Kamil Moskała
697afd52f6 feat(android): add asset management functions to plugin (#4494)
* feat: manage asset plugins

* fix: continue when plugin is not exoplayer plugin

* refactor: change functions plugin type

* docs: introduce new plugins
2025-03-28 13:41:13 +01:00
Kamil Moskała
f188a7fd48 refactor(ios): unpause after exiting fullscreen (#4472)
* refactor(ios): unpause after exiting fullscreen

* chore: lint code
2025-03-26 12:20:32 +01:00
Wojciech Ogrodowczyk
229a5764ea fix(ios): don't try to modify react view after unmount (#4474) 2025-03-22 15:17:41 +01:00
Kamil Moskała
4458627051 docs: update favico & add json-ld (#4477) 2025-03-19 10:57:41 +01:00
Krzysztof Moch
ede502bf74 chore: release v6.11.0 2025-03-16 14:15:57 +01:00
Emmanuel Ferdman
044e08a78d docs: update examples (#4468)
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2025-03-13 20:19:49 +01:00
Lyqqt
d902c1bf43 feat(windows): add topSeek parms mentioned in docs (#4456) 2025-03-13 12:06:36 +01:00
Krzysztof Moch
9f02614f5d refactor(ios): add audio session manager (#4466) 2025-03-12 18:22:50 +01:00
Lyqqt
fa20223c44 fix(windows): event name not matches with SPEC (#4455) 2025-03-12 14:14:23 +01:00
Krzysztof Moch
6e6f91517c feat: enhance react-native-video plugins [Plugins API Breaking] (#4366)
* feat: allow plugins for providing custom DRM manager

This will allow plugins to provide DRM manger to create custom implementations, eg. using SKD from DRM providers

* chore(example): fix drm example on android

* chore: lint code

* fix: remove platform player logic & dependency from `RNVPlugin`

* chore: change warning to debug msg

* chore: lint code

* chore(example/bare): update Podfile.lock

* refactor: reorganize ReactNativeVideoManager plugin registration methods

* refactor: add helpers & clean code

* docs: update documentation

* lint code

* add comment

* docs: update plugins section
2025-03-12 14:13:46 +01:00
Marcin Dziewulski
b5103743e8 fix(android): call startForeground() immediately to prevent ForegroundServiceDidNotStartInTimeException (#4453)
* fix: call startForeground() immediately in onStartCommand() to prevent ForegroundServiceDidNotStartInTimeException

* fix: use string resources for notification text instead of hardcoded strings

* fix: add missing import

* fix: resolve CI issues

* fix: use getString() method to convert string resource IDs to actual strings
2025-03-12 13:42:02 +01:00
Olivier Bouillet
f9d3878ecc chore(ios): remove duplicated code (#4439) 2025-03-12 13:37:03 +01:00
Kamil Moskała
b11c1f315d docs: remove ga (#4462) 2025-03-11 17:42:22 +01:00
Alexander Todorov
bc533e53b0 fix(ios): invalid metadata handling (#4422)
- When the metadata has the tag "iTunSMPB" or "iTunNORM" then the metadata is not converted correctly and comes [nil, nil, ...] which leads to crash

Co-authored-by: Alexander <aleksandar.todorov@serviceos.com>
2025-03-10 22:03:05 +01:00
Krzysztof Moch
9055b14cf4 docs: update structure (#4454)
* [Preview] docs: update  (#9)

Co-authored-by: Bart Widlarz <bart@thewidlarzgroup.com>

* docs: update links

* docs: fix naming

* docs: update footer

* docs: update content

* docs: refine grammar

* fix(docs): update typo

* docs: update url

* refactor: sdk name

* docs: add issue boost link

* docs: add new roadmap to readme

* lint code

---------

Co-authored-by: Bart Widlarz <bart@thewidlarzgroup.com>
Co-authored-by: Kamil <moskalakamil07@gmail.com>
2025-03-06 12:38:25 +01:00
YangJH
82f5f3d21c fix(android): fix bugs related Android PIP listeners (#4441)
* fix(android): fixed bug where listeners were not removed properly

* fix(android): fixes a bug that causes PIP to work when player is null

* refactor(android): call pipUnsubscriber without handler

* chore(android): apply lint
2025-03-06 12:24:59 +01:00
Kamil Moskała
13a5da8955 chore: update utm links (#4447)
* chore: update utm links

* Update package.json
2025-03-04 15:32:40 +01:00
YangJH
235c281219 fix(android): prevent ratio calculations before loading video (#4442) 2025-03-04 15:11:13 +01:00
Philipp Mikheyev
057c287f12 fix(tvOS): handle allowsPictureInPicturePlayback for tvOS (#4448) 2025-03-04 15:08:24 +01:00
Olivier Bouillet
b8459ca71d chore: release v6.10.2 2025-02-22 15:05:17 +01:00
Szymon Rybczak
3edf511f9b docs: remove additional braces in config file (#4418) 2025-02-19 19:22:35 +01:00
Thomas Guenard
6697cbf5d0 fix(android): fix gradle exception text related to AndroidX version (#4420)
* fix(android): fix gradle exception text related to AndroidX version check
* Update android/build.gradle

---------

Co-authored-by: Olivier Bouillet <62574056+freeboub@users.noreply.github.com>
2025-02-19 19:19:41 +01:00
Konstantin
04eec42f1e fix(tvOS): fix tvos compile error for rotation handler (#4417) 2025-02-16 11:55:46 +01:00
Olivier Bouillet
451bafebd7 chore: release v6.10.1 2025-02-15 17:26:27 +01:00
Efstathios Ntonas
a8ca97f05f fix(ios): fix constraints when controls are enabled and video is inside a ScrollView (#4383) 2025-02-15 17:22:06 +01:00
Mihai
c8b800a508 fix(ios): the video has no audio by default (#4409)
This reverts the change in https://github.com/TheWidlarzGroup/react-native-video/pull/4319, I don't think that change is correct.

According to [docs](https://docs.thewidlarzgroup.com/react-native-video/component/props#selectedtexttrack) the default value for `selectedTextTrack.type` is "system" which means the video should respect user's preference in Settings > Accesibility > Subtitles & Captioning > Closed Captions + SDH. And this is exactly what happens by default without the changes in that PR. I think the author of that PR had this setting on, that's why it was showing the subtitles without setting something for `selectedTextTrack`, which IMO is the expected result. In order to explicitly turn off subtitles you have to set `selectedTextTrack={{ type: 'disabled' }}`. I tested it with this video and the settings option on and it works (disables the subtitles) -> https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8

Another reason why I think it should be reverted is because it also affects `selectedAudioTrack`. By default it should also have 'system' type according to [docs](https://docs.thewidlarzgroup.com/react-native-video/component/props#selectedaudiotrack), which means "Play the audio track that matches the system language. If none match, play the first track.", but the change in that PR turns the audio off by default unless you explicitly turn it on with `selectedAudioTrack={{ type: 'system' }}`.

Fixes #4400
2025-02-15 17:09:04 +01:00
Olivier Bouillet
faac5ad456 fix(sample): remove duplicate code in sample (#4391)
* fix: remove duplicated code in sample
2025-02-15 17:05:59 +01:00
Krzysztof Moch
2905b61a0d fix(infra): kotlin linter github action (#4408) 2025-02-09 14:14:40 +01:00
Olivier Bouillet
74b1d5b540 fix(android): remove transparent black overlay on android default controls (#4392) 2025-02-09 14:09:28 +01:00
Mehdi Taghdisi
ad52668d05 fix(android): check for valid width and height on video format data (#4394)
Co-authored-by: mehdi <m.taghdisi@digikala.com>
2025-01-27 18:43:30 +01:00
Olivier Bouillet
638f454a21 fix(android): check androidX version at early build process before really launching build (#4388) 2025-01-25 17:10:47 +01:00
Olivier Bouillet
43955037bb chore: release v6.10.0 2025-01-22 22:59:46 +01:00
Olivier Bouillet
3d3eba97e8 fix(ios): fix paused video by default (#4379)
* fix: update style to allow show control click
* fix: push missing revert
2025-01-22 22:56:50 +01:00
darth levi
d90bf47df5 fix(windows): conversion of string to Stretch enum (#4381) 2025-01-22 22:32:25 +01:00
Olivier Bouillet
d9f92b800e chore: add pre commit hook to lint and tsc the code (#4378) 2025-01-22 22:31:30 +01:00
Hugo EXTRAT
8b952e709a fix(android): support RN 0.77 (#4386) 2025-01-22 22:27:28 +01:00
YangJH
8dc10fd4b7 feat(web): implement web pip method and event (#4370)
Co-authored-by: Olivier Bouillet <62574056+freeboub@users.noreply.github.com>
2025-01-18 15:19:55 +01:00
Olivier Bouillet
449dfb62b5 chore: fix linter (#4377) 2025-01-18 15:05:58 +01:00
YangJH
6c3af99979 fix(tvOS): fix tvos compile error (#4369)
* fix(tvOS): fix tvos compile error
2025-01-18 11:49:12 +01:00
Kamil Moskała
6cc1bff167 docs: fix analytics (#4368) 2025-01-13 15:01:15 +01:00
Krzysztof Moch
89ee02bdab chore: release v6.9.1 2025-01-10 12:09:07 +01:00
Joy
3924b5e295 fix: NPE in setEnterPictureInPictureOnLeave for unsupported Android versions (#4362) 2025-01-10 12:07:25 +01:00
Krzysztof Moch
eff8ea24af infra: fix bot comments (#4367) 2025-01-10 12:01:31 +01:00
Olivier Bouillet
c47d165668 chore(android): fix type in generic folder (#4323) 2025-01-04 13:48:59 +01:00
Vladimir
424f4eedde fix: avoid memory leak on iOS (#4355)
* fix: avoid memory leak on iOS

---------

Co-authored-by: Vladimir Vlasov <crivlaldo@gmail.com>
2025-01-04 13:47:22 +01:00
Krzysztof Moch
d31c72fc04 chore: release v6.9.0 2025-01-04 12:51:27 +01:00
YangJH
69a7bc2d26 feat: implement enterPictureInPictureOnLeave prop for both platform (Android, iOS) (#3385)
* docs: enable Android PIP

* chore: change comments

* feat(android): implement Android PictureInPicture

* refactor: minor refactor code and apply lint

* fix: rewrite pip action intent code for Android14

* fix: remove redundant codes

* feat: add isInPictureInPicture flag for lifecycle handling

- activity provide helper method for same purpose, but this flag makes code simple

* feat: add pipFullscreenPlayerView for makes PIP include video only

* fix: add manifest value checker for prevent crash

* docs: add pictureInPicture prop's Android guide

* fix: sync controller visibility

* refactor: refining variable name

* fix: check multi window mode when host pause

- some OS version call onPause on multi-window mode

* fix: handling when onStop is called while in multi-window mode

* refactor:  enhance PIP util codes

* fix: fix FullscreenPlayerView constructor

* refactor: add enterPictureInPictureOnLeave prop and pip methods

- remove pictureInPicture boolean prop
- add enterPictureInPictureOnLeave boolean prop
- add enterPictureInPicture method
- add exitPictureInPicture method

* fix: fix lint error

* fix: prevent audio play in background without playInBackground prop

* fix: fix onDetachedFromWindow

* docs: update docs for pip

* fix(android): sync pip controller with external controller state

- for media session

* fix(ios): fix pip active fn variable reference

* refactor(ios): refactor code

* refactor(android): refactor codes

* fix(android): fix lint error

* refactor(android): refactor android pip logics

* fix(android): fix flickering issue when stop picture in picture

* fix(android): fix import

* fix(android): fix picture in picture with fullscreen mode

* fix(ios): fix syntax error

* fix(android): fix Fragment managed code

* refactor(android): remove redundant override lifecycle

* fix(js): add PIP type definition for codegen

* fix(android): fix syntax

* chore(android): fix lint error

* fix(ios): fix enter background handler

* refactor(ios): remove redundant code

* fix(ios): fix applicationDidEnterBackground for PIP

* fix(android): fix onPictureInPictureStatusChanged

* fix(ios): fix RCTPictureInPicture

* refactor(android): Ignore exception for some device ignore pip checker

- some device ignore PIP availability check, so we need to handle exception to prevent crash

* fix(android): add hideWithoutPlayer fn into Kotlin ver

* refactor(android): remove redundant code

* fix(android): fix pip ratio to be calculated with correct ratio value

* fix(android): fix crash issue when unmounting in PIP mode

* fix(android): fix lint error

* Update android/src/main/java/com/brentvatne/react/VideoManagerModule.kt

* fix(android): fix lint error

* fix(ios): fix lint error

* fix(ios): fix lint error

* feat(expo): add android picture in picture config within expo plugin

* fix: Replace Fragment with androidx.activity

- remove code that uses Fragment, which is a tricky implementation

* fix: fix lint error

* fix(android): disable auto enter when player released

* fix(android): fix event handler to check based on Activity it's bound to

---------

Co-authored-by: jonghun <jonghun@toss.im>
Co-authored-by: Olivier Bouillet <62574056+freeboub@users.noreply.github.com>
2025-01-04 12:37:33 +01:00
Kamil Moskała
a735a4a581 chore: update bot feature req message (#4341) 2024-12-23 16:28:25 +01:00
Kamil Moskała
78770d92f5 refactor: make twg badge visible on smaller screens (#4345) 2024-12-21 08:51:45 +01:00
Kamil Moskała
abc4d76099 docs: link tv example (#4342) 2024-12-18 09:24:21 +01:00
Kamil Moskała
16fa20411f docs: update useful projects list (#4337) 2024-12-14 16:15:18 +01:00
Giovanni P.
3da4f1ca97 fix(ios): _paused is updated when video playback pause (#4320) 2024-12-10 19:50:32 +01:00
Kamil Moskała
7b4bd9a016 fix(docs): bump next.js version & fix meta warnings (#4327)
* chore(docs): bump `next` version

* docs: fix meta warnings
2024-12-10 19:46:55 +01:00
7a6afd52a3 Handle seek completion
Some checks failed
Build Android / Build Android Example App (pull_request) Has been cancelled
Build Android / Build Android Example App Without Ads (pull_request) Has been cancelled
Build iOS / Build iOS Example App (pull_request) Has been cancelled
Build iOS / Build iOS Example App With Ads (pull_request) Has been cancelled
Build iOS / Build iOS Example App With Caching (pull_request) Has been cancelled
Check Android / Kotlin-Lint (pull_request) Has been cancelled
Check CLang / CLang-Format (pull_request) Has been cancelled
Check iOS / Swift-Lint (pull_request) Has been cancelled
Check iOS / Swift-Format (pull_request) Has been cancelled
Check JS / Check TS (tsc) (pull_request) Has been cancelled
Check JS / Lint JS (eslint, prettier) (pull_request) Has been cancelled
Test Docs build / build-docs (pull_request) Has been cancelled
2024-12-04 12:51:28 -07:00
proohit
1033c9d4f3 fix(ios): disables subtitles for none and empty track types (#4319) 2024-12-04 15:35:51 +01:00
Chris Wood
d757a44bb1 Docs(README): correct Expo heading link (#4316) 2024-12-03 16:32:13 +01:00
YangJH
64c222df44 chore(android): bump default androidx.activity version from v1.8.2 to v1.9.3 (#4314) 2024-12-01 13:46:56 +01:00
Kamil Moskała
621a80299c fix: hiding poster (#4308)
* fix: hiding poster

* fix: hiding poster

* remove zIndex: 1

* fix: remove showPoster from dependency array
2024-12-01 13:41:03 +01:00
Olivier Bouillet
63c592f7cd fix(android): disable caching on local asset files (#4304) 2024-12-01 13:29:24 +01:00
Olivier Bouillet
569a79c510 chore: release v6.8.2 2024-11-25 21:36:30 +01:00
Olivier Bouillet
f37dc9e33e fix: playback restart without bufferingConfig (#4305) 2024-11-25 21:33:56 +01:00
Olivier Bouillet
dd78241b0d chore: release v6.8.1 2024-11-24 21:25:46 +01:00
Olivier Bouillet
2b7c215e66 Fix(android): restart issue react76 (#4302)
* fix: upgrade to expo 54
* fix: more bufferConfig inside source
- restart issue on react 0.76
- fix constness
- deprecate bufferConfig in root props
- update documentation
2024-11-24 21:19:46 +01:00
Tarık
daaac9740a fix(ios): handle async player access in text track selection (#4293)
* fix(ios): add null check to setSelectedTextTrack for player instance

* Revert "fix(ios): add null check to setSelectedTextTrack for player instance"

This reverts commit 447c83423cdd77b0cfa9cc171b231327a2cf1586.

* fix(ios): ensure strong reference to player during async operation

* fix: linter

* fix: linter formatting

* fix: revert typo
2024-11-22 14:19:09 +01:00
Krzysztof Moch
d934f214f5 chore: release v6.8.0 2024-11-17 15:42:49 +01:00
Islam
d277c5e946 Add support for HLS videos caching on android (#4272)
* Add support for HLS videos caching on android
2024-11-15 21:18:25 +01:00
Kamil Moskała
de81829d73 chore: cleanup ref (#4288)
* refactor: cleanup ref
* fix: video-ref type
* refactor: cleanup code
2024-11-15 21:17:56 +01:00
Olivier Bouillet
681aed0aed chore(sample): fix run of expo sample (#4284)
* chore: fix expo sample tun
* chore: update ios sample files
2024-11-15 21:17:29 +01:00
Zoe Roux
5fa77c4562 Add react-native-web support (#3958)
Co-authored-by: Kamil Moskała <91079590+moskalakamil@users.noreply.github.com>
2024-11-13 21:19:57 +01:00
Krzysztof Moch
d45300270e chore: add FUNDING.yml 2024-11-08 16:09:49 +01:00
Kamil Moskała
0288d61e46 chore: update docs link (#4242) 2024-11-07 09:28:20 +01:00
Krzysztof Moch
f78a407434 chore(ci): bump actions versions (#4275) 2024-11-06 11:51:27 +01:00
Kamil Moskała
6080e96e82 docs: update drm token generator links (#4263) 2024-10-29 10:31:50 +01:00
Krzysztof Moch
5b3add4e7a chore: update CONTRIBUTING.md 2024-10-26 15:16:41 +02:00
Krzysztof Moch
f850b7a71c chore: update DRM example (#4259) 2024-10-26 14:55:11 +02:00
Olivier Bouillet
3b4bfd3936 fix(android): add helper to avoid type error (#4257)
* fix(android): add helper to avoid type error
2024-10-24 11:43:10 +02:00
Krzysztof Moch
21e2da3bfb chore: update README 2024-10-21 15:22:46 +02:00
Krzysztof Moch
5d532f5982 chore: update README 2024-10-21 15:20:23 +02:00
Krzysztof Moch
324fc2ac65 chore: update examples README 2024-10-20 20:31:30 +02:00
Krzysztof Moch
9eb5502076 chore: rework examples (#4225)
* remove unused examples

* init bare example with test app

* add react-native-video

* add test app suport in expo plugin

* expo plugin: skip keys that are already in pod file

* fix podfile

* add src files

* fix metro config

* finalize react native test app configuration

* init expo example

* remove old examples

* add guide for example

* Add link to examples apps in docs

* adopt bare example to CI tests

* update CI workflows

* CI build lib after node_modules install

* fix examples readme

* fix iOS CI

* Add Example for DRM

* Update examples/README.md

* fix links

* update examples README

* sync example code

* update README
2024-10-20 20:04:02 +02:00
d7977241c9 Account for crop in progress 2024-10-18 03:25:50 -06:00
921ead0f05 Timeout actions 2024-10-17 19:21:06 -06:00
20397d32e6 More logging 2024-10-17 19:11:22 -06:00
e3900e794d What is going on 2024-10-17 19:07:39 -06:00
4dc7bf465f Typescript 2024-10-17 18:59:42 -06:00
e5f182cda9 Use an async queue 2024-10-17 18:56:38 -06:00
Olivier Bouillet
7c7d83b6e5 chore: release v6.7.0 2024-10-17 21:58:53 +02:00
Olivier Bouillet
7501880062 fix: remove warning and refactor & fix ad workflow (#4235) 2024-10-17 21:56:06 +02:00
Olivier Bouillet
f04b233a40 chore: move minLoadRetryCount into source property (#4233) 2024-10-16 23:41:22 +02:00
559 changed files with 15386 additions and 112587 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: TheWidlarzGroup

View File

@@ -54,6 +54,6 @@ body:
value: |
## Support
If this functionality is important to you and you need it, contact [TheWidlarzGroup](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=feature-request#Contact) - [`hi@thewidlarzgroup.com`](mailto:hi@thewidlarzgroup.com)
If this functionality is important to you and you need it, contact [TheWidlarzGroup](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=feature-request&utm_campaign=issue-template&utm_id=sponsorship#Contact) - [`hi@thewidlarzgroup.com`](mailto:hi@thewidlarzgroup.com)

View File

@@ -17,7 +17,7 @@ runs:
- name: Cache dependencies
id: bun-cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
**/node_modules

View File

@@ -13,11 +13,11 @@ runs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 20.x
- name: Cache dependencies
id: yarn-cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
${{ inputs.working-directory }}/node_modules

View File

@@ -25,28 +25,40 @@ const BOT_LABELS = [
'Missing Repro',
'Waiting for Review',
'Newer Version Available',
'6.x.x',
'7.0',
...Object.values(PLATFORM_LABELS),
];
const SKIP_LABEL = 'No Validation';
const ISSUE_BOOST_INFO = (issueNumber) => `
Need faster resolution? Consider [Issue Boost](https://www.thewidlarzgroup.com/issue-boost/?utm_source=rnv&utm_medium=bug-report&utm_campaign=bot-message&utm_id=${issueNumber}) it allows us to dedicate time specifically to your issue and fix it faster 🚀`;
const MESSAGE = {
FEATURE_REQUEST: `Thank you for your feature request. We will review it and get back to you if we need more information.`,
BUG_REPORT: `Thank you for your bug report. We will review it and get back to you if we need more information.`,
FEATURE_REQUEST: (issueNumber) => `Thanks for the feature request! 🚀
\nYou can check out our [public roadmap](https://github.com/orgs/TheWidlarzGroup/projects/6) to see what we're currently working on. All requests are automatically added there, so you can track progress anytime.
\nWe review and implement new features when time allows, but this can take a while. If you'd like to speed things up and make this a priority, consider [Issue Boost](https://www.thewidlarzgroup.com/issue-boost/?utm_source=rnv&utm_medium=feature-request&utm_campaign=bot-message&utm_id=${issueNumber}), our commercial option that lets us dedicate time specifically to your request.
\nThanks for your input and patience! 🙌`,
BUG_REPORT: (issueNumber) => `Hey! 👋
Thanks for reporting this issue. We try to fix bugs as quickly as possible, but since our time is limited, we prioritize sponsored issues first, then focus on critical problems affecting many users, and finally, we handle other reports when we can. Some issues might take a while to be resolved.
\nIf you want to speed up this process, check out [Issue Boost](https://www.thewidlarzgroup.com/issue-boost/?utm_source=rnv&utm_medium=bug-report&utm_campaign=bot-message-valid&utm_id=${issueNumber}) it allows us to dedicate time specifically to your issue and fix it faster.
\nThanks for your patience and support! 🚀`,
MISSING_INFO: (missingFields) => {
return `Thank you for your issue report. Please note that the following information is missing or incomplete:\n\n${missingFields
return `Hey! 👋
Thanks for the bug report. To help us resolve your issue effectively, we still need some key information:\n\n${missingFields
.map((field) => `- ${field.replace('missing-', '')}`)
.join(
'\n',
)}\n\nPlease update your issue with this information to help us address it more effectively.
\n > Note: issues without complete information have a lower priority`;
.join('\n')}
Please edit your issue and fill in the missing details.
> Issues with incomplete info are treated with lower priority, so this helps speed things up.`;
},
OUTDATED_VERSION: (issueVersion, latestVersion) => {
return (
`There is a newer version of the library available. ` +
`You are using version ${issueVersion}, while the latest stable version is ${latestVersion}. ` +
`Please update to the latest version and check if the issue still exists.` +
`\n > Note: If the issue still exists, please update the issue report with the latest information.`
`Heads up! ⚠️ You're using version **${issueVersion}**, but the latest stable version is **${latestVersion}**. ` +
`Please update to the newest version and check if the issue still occurs.\n\n` +
`> Keeping your dependencies up-to-date often resolves many common problems.` +
`\n\nStill having the issue after upgrading? Update the report with the new version details so we can investigate.`
);
},
};
@@ -114,6 +126,17 @@ const validateBugReport = async (body, labels) => {
if (!isVersionValid) {
labels.add('missing-version');
} else {
// Add version-specific labels
const versionMatch = words.find((word) => versionPattern.test(word));
if (versionMatch) {
const majorVersion = versionMatch.split('.')[0];
if (majorVersion === '6') {
labels.add('6.x.x');
} else if (majorVersion === '7') {
labels.add('7.0');
}
}
}
const latestVersion = await checkLatestVersion();
@@ -178,7 +201,8 @@ const handleIssue = async ({github, context}) => {
const handleFeatureRequest = async ({github, context, body, labels}) => {
validateFeatureRequest(body, labels);
const comment = MESSAGE.FEATURE_REQUEST;
const comment = MESSAGE.FEATURE_REQUEST(context.payload.issue.number);
await hidePreviousComments({github, context});
await createComment({github, context, body: comment});
};
@@ -212,6 +236,8 @@ const handleMissingInformation = async ({github, context, labels}) => {
)}`;
}
comment += `\n\n${ISSUE_BOOST_INFO(context.payload.issue.number)}`;
await hidePreviousComments({github, context});
await createComment({github, context, body: comment});
}
@@ -220,7 +246,7 @@ const handleMissingInformation = async ({github, context, labels}) => {
};
const handleValidReport = async ({github, context, labels}) => {
let comment = MESSAGE.BUG_REPORT;
let comment = MESSAGE.BUG_REPORT(context.payload.issue.number);
const outdatedVersionLabel = Array.from(labels).find((label) =>
label.startsWith('outdated-version'),
@@ -267,11 +293,15 @@ const hidePreviousComments = async ({github, context}) => {
issue_number: context.payload.issue.number,
});
const botComments = comments.data.filter(
(comment) => comment.user.type === 'Bot',
// Filter for bot comments that aren't already hidden
const unhiddenBotComments = comments.data.filter(
(comment) =>
comment.user.type === 'Bot' &&
!comment.body.includes('<details>') &&
!comment.body.includes('Previous bot comment'),
);
for (const comment of botComments) {
for (const comment of unhiddenBotComments) {
// Don't format string - it will broke the markdown
const hiddenBody = `
<details>

View File

@@ -7,16 +7,16 @@ on:
paths:
- '.github/workflows/build-android.yml'
- 'android/**'
- 'examples/basic/android/**'
- 'examples/bare/android/**'
- 'yarn.lock'
- 'examples/basic/yarn.lock'
- 'examples/bare/yarn.lock'
pull_request:
paths:
- '.github/workflows/build-android.yml'
- 'android/**'
- 'examples/basic/android/**'
- 'examples/bare/android/**'
- 'yarn.lock'
- 'examples/basic/yarn.lock'
- 'examples/bare/yarn.lock'
jobs:
build:
@@ -32,13 +32,21 @@ jobs:
java-version: 17
java-package: jdk
- name: Install node_modules
- name: Install node_modules at Root
uses: ./.github/actions/setup-node
with:
working-directory: examples/basic
working-directory: ./
- name: Build Library
run: yarn build
- name: Install node_modules at Example
uses: ./.github/actions/setup-node
with:
working-directory: examples/bare
- name: Restore Gradle cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
@@ -46,11 +54,11 @@ jobs:
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Run Gradle Build for basic example
run: cd examples/basic/android && ./gradlew assembleDebug --build-cache && cd ../../..
- name: Run Gradle Build for bare example
run: cd examples/bare/android && ./gradlew assembleDebug --build-cache && cd ../../..
build-without-ads:
name: Build Android Example App Without Ads
build-with-ads:
name: Build Android Example App With Ads
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -62,13 +70,21 @@ jobs:
java-version: 17
java-package: jdk
- name: Install node_modules
- name: Install node_modules at Root
uses: ./.github/actions/setup-node
with:
working-directory: examples/basic
working-directory: ./
- name: Build Library
run: yarn build
- name: Install node_modules at Example
uses: ./.github/actions/setup-node
with:
working-directory: examples/bare
- name: Restore Gradle cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
@@ -76,5 +92,5 @@ jobs:
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Run Gradle Build for basic example
run: cd examples/basic/android && export RNV_SAMPLE_ENABLE_ADS=false && ./gradlew assembleDebug --build-cache && cd ../../..
- name: Run Gradle Build for bare example
run: cd examples/bare/android && export RNV_SAMPLE_ENABLE_ADS=true && ./gradlew assembleDebug --build-cache && cd ../../..

View File

@@ -9,28 +9,37 @@ on:
- '.github/workflows/build-ios.yml'
- 'ios/**'
- '*.podspec'
- 'examples/basic/ios/**'
- 'examples/bare/ios/**'
pull_request:
paths:
- '.github/workflows/build-ios.yml'
- 'ios/**'
- '*.podspec'
- 'examples/basic/ios/**'
- 'examples/bare/ios/**'
jobs:
build:
name: Build iOS Example App
runs-on: macos-14 # This allow us to use Xcode 15.0.1 which is a lot faster - TODO change to "macos-latest" once it's out of beta
runs-on: macos-latest
defaults:
run:
working-directory: examples/basic/ios
working-directory: examples/bare/ios
steps:
- uses: actions/checkout@v4
- name: Install node_modules
- name: Install node_modules at Root
uses: ./.github/actions/setup-node
with:
working-directory: examples/basic
working-directory: ./
- name: Build Library
working-directory: ./
run: yarn build
- name: Install node_modules at Example
uses: ./.github/actions/setup-node
with:
working-directory: examples/bare
- name: Restore buildcache
uses: mikehardy/buildcache-action@v2
@@ -43,43 +52,58 @@ jobs:
bundler-cache: true
- name: Restore Pods cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
examples/basic/ios/Pods
examples/bare/ios/Pods
~/Library/Caches/CocoaPods
~/.cocoapods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-
- name: Generate Native Project
run: pod install
- name: Install Pods
run: pod install
- name: Install xcpretty
run: gem install xcpretty
- name: Build App
run: "set -o pipefail && xcodebuild \
-derivedDataPath build -UseModernBuildSystem=YES \
-workspace videoplayer.xcworkspace \
-scheme videoplayer \
-workspace BareExample.xcworkspace \
-scheme BareExample \
-sdk iphonesimulator \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 14' \
-destination 'platform=iOS Simulator,name=iPhone 16' \
build \
CODE_SIGNING_ALLOWED=NO | xcpretty"
build-with-ads:
name: Build iOS Example App With Ads
runs-on: macos-14 # This allow us to use Xcode 15.0.1 which is a lot faster - TODO change to "macos-latest" once it's out of beta
runs-on: macos-latest
defaults:
run:
working-directory: examples/basic/ios
working-directory: examples/bare/ios
steps:
- uses: actions/checkout@v4
- name: Install node_modules
- name: Install node_modules at Root
uses: ./.github/actions/setup-node
with:
working-directory: examples/basic
working-directory: ./
- name: Build Library
working-directory: ./
run: yarn build
- name: Install node_modules at Example
uses: ./.github/actions/setup-node
with:
working-directory: examples/bare
- name: Restore buildcache
uses: mikehardy/buildcache-action@v2
@@ -92,43 +116,58 @@ jobs:
bundler-cache: true
- name: Restore Pods cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
examples/basic/ios/Pods
examples/bare/ios/Pods
~/Library/Caches/CocoaPods
~/.cocoapods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-
- name: Generate Native Project
run: export RNV_SAMPLE_ENABLE_ADS=true && pod install
- name: Install Pods
run: export RNV_SAMPLE_ENABLE_ADS=true && pod install
- name: Install xcpretty
run: gem install xcpretty
- name: Build App
run: "set -o pipefail && export RNV_SAMPLE_ENABLE_ADS=true && xcodebuild \
-derivedDataPath build -UseModernBuildSystem=YES \
-workspace videoplayer.xcworkspace \
-scheme videoplayer \
-workspace BareExample.xcworkspace \
-scheme BareExample \
-sdk iphonesimulator \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 14' \
-destination 'platform=iOS Simulator,name=iPhone 16' \
build \
CODE_SIGNING_ALLOWED=NO | xcpretty"
build-with-caching:
name: Build iOS Example App With Caching
runs-on: macos-14 # This allow us to use Xcode 15.0.1 which is a lot faster - TODO change to "macos-latest" once it's out of beta
runs-on: macos-latest
defaults:
run:
working-directory: examples/basic/ios
working-directory: examples/bare/ios
steps:
- uses: actions/checkout@v4
- name: Install node_modules
- name: Install node_modules at Root
uses: ./.github/actions/setup-node
with:
working-directory: examples/basic
working-directory: ./
- name: Build Library
working-directory: ./
run: yarn build
- name: Install node_modules at Example
uses: ./.github/actions/setup-node
with:
working-directory: examples/bare
- name: Restore buildcache
uses: mikehardy/buildcache-action@v2
@@ -141,26 +180,35 @@ jobs:
bundler-cache: true
- name: Restore Pods cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
examples/basic/ios/Pods
examples/bare/ios/Pods
~/Library/Caches/CocoaPods
~/.cocoapods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-
- name: Install gem dependencies
run: bundle install
- name: Generate Native Project
run: export RNV_SAMPLE_VIDEO_CACHING=true && bundle exec pod install
- name: Install Pods
run: export RNV_SAMPLE_VIDEO_CACHING=true && pod install
run: export RNV_SAMPLE_VIDEO_CACHING=true && bundle exec pod install
- name: Install xcpretty
run: gem install xcpretty
- name: Build App
run: "set -o pipefail && export RNV_SAMPLE_VIDEO_CACHING=true && xcodebuild \
-derivedDataPath build -UseModernBuildSystem=YES \
-workspace videoplayer.xcworkspace \
-scheme videoplayer \
-workspace BareExample.xcworkspace \
-scheme BareExample \
-sdk iphonesimulator \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 14' \
-destination 'platform=iOS Simulator,name=iPhone 16' \
build \
CODE_SIGNING_ALLOWED=NO | xcpretty"

View File

@@ -28,7 +28,7 @@ jobs:
with:
report-path: ./android/build/*.xml
continue-on-error: false
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: ktlint-report
path: ./android/build/*.xml

View File

@@ -1,46 +0,0 @@
name: deploy docs
on:
workflow_dispatch:
push:
branches:
- master
paths:
- '.github/workflows/deploy-docs.yml'
- 'docs/**'
jobs:
deploy-docs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup-bun
with:
working-directory: ./docs
- name: Cache build
uses: actions/cache@v3
with:
path: |
docs/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}
${{ runner.os }}-nextjs-
- name: Build docs
run: |
bun --cwd docs build
touch docs/out/.nojekyll
- name: Deploy docs to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: gh-pages
folder: docs/out
permissions:
contents: write

View File

@@ -1,35 +0,0 @@
name: Test Docs build
on:
workflow_dispatch:
pull_request:
paths:
- '.github/workflows/test-build-docs.yml'
- 'docs/**'
jobs:
build-docs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup-bun
with:
working-directory: ./docs
- name: Cache build
uses: actions/cache@v3
with:
path: |
docs/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}
${{ runner.os }}-nextjs-
- name: Build docs
run: |
bun --cwd docs build
touch docs/out/.nojekyll

View File

@@ -8,7 +8,7 @@ jobs:
validate-and-label:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Validate Issue Template and Add Labels
uses: actions/github-script@v7

4
.gitmodules vendored Normal file
View File

@@ -0,0 +1,4 @@
[submodule "examples/react-native-offline-video-download-starter"]
path = examples/react-native-offline-video-download-starter
url = https://github.com/TheWidlarzGroup/react-native-offline-video-starter.git
branch = main

2
.husky/pre-push Normal file
View File

@@ -0,0 +1,2 @@
echo "precommit"
yarn check-all

View File

@@ -1,4 +1,262 @@
# Changelog
## [6.19.2](https://github.com/moskalakamil/react-native-video/compare/v6.19.1...v6.19.2) (2026-04-28)
### Bug Fixes
* replace `absoluteFillObject` with `absoluteFill` ([#4880](https://github.com/moskalakamil/react-native-video/issues/4880)) ([ae7ff4f](https://github.com/moskalakamil/react-native-video/commit/ae7ff4fa07ce9234575664a541bbd2f557ff051c))
## [6.19.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.19.0...v6.19.1) (2026-03-15)
### Bug Fixes
* **ios:** IMA ad container resizing on orientation change ([#4771](https://github.com/TheWidlarzGroup/react-native-video/issues/4771)) ([fc936c4](https://github.com/TheWidlarzGroup/react-native-video/commit/fc936c49ef3c2734173442042b7d5038aeaef301))
* RCTVideoManager crash in bridgeless mode RN0.84 ([#4855](https://github.com/TheWidlarzGroup/react-native-video/issues/4855)) ([92b0a0e](https://github.com/TheWidlarzGroup/react-native-video/commit/92b0a0e416c7f313120a811cd2dc972f87b1c82f))
# [6.19.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.18.0...v6.19.0) (2026-01-19)
### Bug Fixes
* **android:** correct videoTrack type definitions to match Android implementation ([#4778](https://github.com/TheWidlarzGroup/react-native-video/issues/4778)) ([commit](https://github.com/TheWidlarzGroup/react-native-video/commit/f38717778515b06c462fe75dcd94d2cce5ba3f95))
### Features
* **BREAKING CHANGE:** add DAI support ([#4816](https://github.com/TheWidlarzGroup/react-native-video/issues/4816)) ([commit](https://github.com/TheWidlarzGroup/react-native-video/commit/88ac1ae1dcdc907415f806bd64bd3d0a92ccd7d1))
# [6.18.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.17.0...v6.18.0) (2025-11-18)
### Bug Fixes
* **android:** prevent duplicate `onVideoEnd` callback on prop changes ([#4762](https://github.com/TheWidlarzGroup/react-native-video/issues/4762)) ([05cd597](https://github.com/TheWidlarzGroup/react-native-video/commit/05cd5972c21ebcacf3cd5952e92f121a84b5c9a9))
* **ci:** update ios device for builds ([#4757](https://github.com/TheWidlarzGroup/react-native-video/issues/4757)) ([a9f7524](https://github.com/TheWidlarzGroup/react-native-video/commit/a9f752435f3d94abfdb37ef5dc9c444038e3ad6a))
* entering PiP mode when controls are true ([#4776](https://github.com/TheWidlarzGroup/react-native-video/issues/4776)) ([ba65ab1](https://github.com/TheWidlarzGroup/react-native-video/commit/ba65ab123321713e537fc7ccb1ac3ed5676f1677))
* **iOS:** use top-most presented view controller for fullscreen presentation on iOS ([#4753](https://github.com/TheWidlarzGroup/react-native-video/issues/4753)) ([5d75b48](https://github.com/TheWidlarzGroup/react-native-video/commit/5d75b482952a9cd3e5f59237e302137857739d4e))
* prevent `audiovisualBackgroundPlaybackPolicy` crash ([#4763](https://github.com/TheWidlarzGroup/react-native-video/issues/4763)) ([fbb260e](https://github.com/TheWidlarzGroup/react-native-video/commit/fbb260e9164194a55d2b26404aea000e924e2f04))
### Features
* **ios:** add PublicAudioSessionManager for audio session management ([#4747](https://github.com/TheWidlarzGroup/react-native-video/issues/4747)) ([f2afd16](https://github.com/TheWidlarzGroup/react-native-video/commit/f2afd16d0bc7fc72e0b4d8400d74342244158674))
# [6.17.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.16.1...v6.17.0) (2025-10-06)
### Bug Fixes
* android control option is not work when init video ([#4698](https://github.com/TheWidlarzGroup/react-native-video/issues/4698)) ([5074ca5](https://github.com/TheWidlarzGroup/react-native-video/commit/5074ca5a1b1ae687255f3509fd75cc847ace0dca))
* **android:** catch errors from `activity.packageManager` ([5b59b06](https://github.com/TheWidlarzGroup/react-native-video/commit/5b59b06d1e19f138a98648d06a931ab4c634cb75))
* **ios:** add audiovisualBackgroundPlaybackPolicy ([#4570](https://github.com/TheWidlarzGroup/react-native-video/issues/4570)) ([bf81038](https://github.com/TheWidlarzGroup/react-native-video/commit/bf810386b9ee1aed579b24adffb02f4acfa413db))
* **ios:** disable audio session management when no views ([#4666](https://github.com/TheWidlarzGroup/react-native-video/issues/4666)) ([d2c92a1](https://github.com/TheWidlarzGroup/react-native-video/commit/d2c92a1f3f6579aa6607389de8e51e4b75012f3b))
* **player:** trigger relayout after exiting pip ([#4665](https://github.com/TheWidlarzGroup/react-native-video/issues/4665)) ([4b996fc](https://github.com/TheWidlarzGroup/react-native-video/commit/4b996fc514b015dfb848dd2a0226cda4b0c527fb))
### Features
* **android:** enable flexible page sizes in native build configuration ([#4691](https://github.com/TheWidlarzGroup/react-native-video/issues/4691)) ([7d233f4](https://github.com/TheWidlarzGroup/react-native-video/commit/7d233f414f8a00734d3991f2e1d148bbadae0f0e))
* **android:** report full native stack trace on error ([#4651](https://github.com/TheWidlarzGroup/react-native-video/issues/4651)) ([724f639](https://github.com/TheWidlarzGroup/react-native-video/commit/724f63930d2de3c81b9d9316ae70bbb9e1dcae49))
* **examples:** add offline-video-starter as submodule ([#4644](https://github.com/TheWidlarzGroup/react-native-video/issues/4644)) ([60baecd](https://github.com/TheWidlarzGroup/react-native-video/commit/60baecdf739baa3ce9a84b3a02cd4e5b9060275f))
## [6.16.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.16.0...v6.16.1) (2025-07-08)
# [6.16.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.15.0...v6.16.0) (2025-07-02)
### Bug Fixes
* **android:** create custom event class for dispatcher ([#4575](https://github.com/TheWidlarzGroup/react-native-video/issues/4575)) ([94967fc](https://github.com/TheWidlarzGroup/react-native-video/commit/94967fc4a61e00c521242d29fd28bed13bbba7c4))
* **android:** speed control is not applied ([#4572](https://github.com/TheWidlarzGroup/react-native-video/issues/4572)) ([b56b647](https://github.com/TheWidlarzGroup/react-native-video/commit/b56b647d85aa24df2ba2ad436eb5ada67735417f))
* **ios:** allow audio mixing when none of the players are playing ([#4579](https://github.com/TheWidlarzGroup/react-native-video/issues/4579)) ([2d7e039](https://github.com/TheWidlarzGroup/react-native-video/commit/2d7e03942d5e2525e5c71dc914926ba07f67d54a))
* **ios:** retain cycle and memory leak involving the RCTVideo instance when using Google IMA ads in react-native-video. ([#4574](https://github.com/TheWidlarzGroup/react-native-video/issues/4574)) ([b51b579](https://github.com/TheWidlarzGroup/react-native-video/commit/b51b579ff00067231fb1d0754e1a86692f44bc50))
### Features
* **android:** replace custom VideoView with media3 `PlayerView` ([#4581](https://github.com/TheWidlarzGroup/react-native-video/issues/4581)) ([978683b](https://github.com/TheWidlarzGroup/react-native-video/commit/978683b64582e6363d7b5a1817e22fec342d1c47))
# [6.15.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.14.1...v6.15.0) (2025-06-12)
### Bug Fixes
* **web:** add missing component type ([9f03cc5](https://github.com/TheWidlarzGroup/react-native-video/commit/9f03cc5a0983e05515d3d194204a7c9eb71b383d))
### Features
* allow audio mixing if one of the video views require it ([#4559](https://github.com/TheWidlarzGroup/react-native-video/issues/4559)) ([3576a13](https://github.com/TheWidlarzGroup/react-native-video/commit/3576a134e678e6273d9195d7a17c9828c30426e2))
* **ios:** forward real fullscreen events from AVPlayer instead of guessing ([#4509](https://github.com/TheWidlarzGroup/react-native-video/issues/4509)) ([88c20d1](https://github.com/TheWidlarzGroup/react-native-video/commit/88c20d1c065b2778a66ddde4a3b92bd58bec043b))
* **plugin:** overrideMediaSourceFactory ([#4566](https://github.com/TheWidlarzGroup/react-native-video/issues/4566)) ([9cf7802](https://github.com/TheWidlarzGroup/react-native-video/commit/9cf780276af7f0a890025a13e1685e49c6589f0e))
## [6.14.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.14.0...v6.14.1) (2025-05-28)
### Features
* **android:** allow plugins to override drm session manager ([#4558](https://github.com/TheWidlarzGroup/react-native-video/issues/4558)) ([9449eb3](https://github.com/TheWidlarzGroup/react-native-video/commit/9449eb34f3ebcf7ac08bde1ee55e5cc26a142217))
# [6.14.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.13.0...v6.14.0) (2025-05-10)
### Bug Fixes
* **ios:** default cropStart fallback ([#4540](https://github.com/TheWidlarzGroup/react-native-video/issues/4540)) ([ecfe12a](https://github.com/TheWidlarzGroup/react-native-video/commit/ecfe12aa816a3126e023d0c14896681d92fe8926))
* **ios:** set `_isBuffering = true` only if newValue is also true ([#4532](https://github.com/TheWidlarzGroup/react-native-video/issues/4532)) ([089e938](https://github.com/TheWidlarzGroup/react-native-video/commit/089e938aebc222378f4d16006f585f20a8b0eed1))
* **ios:** swfit modular headers ([#4527](https://github.com/TheWidlarzGroup/react-native-video/issues/4527)) ([987be4b](https://github.com/TheWidlarzGroup/react-native-video/commit/987be4b293dc21a40b0b183433707bc9a733b715))
### Features
* **web:** allow `style` prop overrides ([#4528](https://github.com/TheWidlarzGroup/react-native-video/issues/4528)) ([fc1e3f4](https://github.com/TheWidlarzGroup/react-native-video/commit/fc1e3f4fd17faf1503b6b3d7cc604dbe1bc659c1))
# [6.13.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.12.0...v6.13.0) (2025-04-18)
### Bug Fixes
* **macCatalyst:** allowsVideoFrameAnalysis not available in catalyst until 18.0 ([#4515](https://github.com/TheWidlarzGroup/react-native-video/issues/4515)) ([b17c319](https://github.com/TheWidlarzGroup/react-native-video/commit/b17c319c32ad8080ad911ec4be7fa02bc4a6d6ee))
* **tvos:** build ([#4511](https://github.com/TheWidlarzGroup/react-native-video/issues/4511)) ([4034046](https://github.com/TheWidlarzGroup/react-native-video/commit/40340467d7df2ad606f25606041630c489c385f6))
### Features
* **ios:** add `overridePlayerAsset` to `AVPlayerPlugin` ([#4522](https://github.com/TheWidlarzGroup/react-native-video/issues/4522)) ([b1b3db3](https://github.com/TheWidlarzGroup/react-native-video/commit/b1b3db301097e82c8d449c08f7d334a2eaeac0ea))
# [6.12.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.11.0...v6.12.0) (2025-04-06)
### Bug Fixes
* add extra checks to determine if a file is local ([#4503](https://github.com/TheWidlarzGroup/react-native-video/issues/4503)) ([a849cc1](https://github.com/TheWidlarzGroup/react-native-video/commit/a849cc19e8dafe0cc8147aeb8a226fc1373bb84d))
* **ios:** don't try to modify react view after unmount ([#4474](https://github.com/TheWidlarzGroup/react-native-video/issues/4474)) ([229a576](https://github.com/TheWidlarzGroup/react-native-video/commit/229a5764ea45ea74e5d469aaac30057c201bd228))
### Features
* **android:** add asset management functions to plugin ([#4494](https://github.com/TheWidlarzGroup/react-native-video/issues/4494)) ([697afd5](https://github.com/TheWidlarzGroup/react-native-video/commit/697afd52f60cbd52660843b8d29228a2ff7d0060))
* **android:** initial bitrate ([#4480](https://github.com/TheWidlarzGroup/react-native-video/issues/4480)) ([41ddc5c](https://github.com/TheWidlarzGroup/react-native-video/commit/41ddc5c27a9c180a6d0364b3f3286211f7a10e68))
* **ios:** allow to disable audio sessions management ([#4492](https://github.com/TheWidlarzGroup/react-native-video/issues/4492)) ([8836362](https://github.com/TheWidlarzGroup/react-native-video/commit/8836362609d226f795b01e1c65a70b9c4ecde1e5))
* **ios:** set playback speed controls to initial playback rate ([#4495](https://github.com/TheWidlarzGroup/react-native-video/issues/4495)) ([d2e5d9c](https://github.com/TheWidlarzGroup/react-native-video/commit/d2e5d9c64eeb80143c39ca39fcdc8ba65f40a400))
# [6.11.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.10.2...v6.11.0) (2025-03-16)
### Bug Fixes
* **android:** call `startForeground()` immediately to prevent `ForegroundServiceDidNotStartInTimeException` ([#4453](https://github.com/TheWidlarzGroup/react-native-video/issues/4453)) ([b510374](https://github.com/TheWidlarzGroup/react-native-video/commit/b5103743e87a6ef24c69a07a8368415ff0fc7886))
* **android:** fix bugs related Android PIP listeners ([#4441](https://github.com/TheWidlarzGroup/react-native-video/issues/4441)) ([82f5f3d](https://github.com/TheWidlarzGroup/react-native-video/commit/82f5f3d21c2f29282c024bfa3a8f9562a4ed8e0f))
* **android:** prevent ratio calculations before loading video ([#4442](https://github.com/TheWidlarzGroup/react-native-video/issues/4442)) ([235c281](https://github.com/TheWidlarzGroup/react-native-video/commit/235c28121903e20f58d4d830fea5929d2e6e0b56))
* **ios:** invalid metadata handling ([#4422](https://github.com/TheWidlarzGroup/react-native-video/issues/4422)) ([bc533e5](https://github.com/TheWidlarzGroup/react-native-video/commit/bc533e53b0b4c6906f6e72eac7e3f228da3e4bbd))
* **tvOS:** handle allowsPictureInPicturePlayback for tvOS ([#4448](https://github.com/TheWidlarzGroup/react-native-video/issues/4448)) ([057c287](https://github.com/TheWidlarzGroup/react-native-video/commit/057c287f127b9f796e592ae538478386f42a447c))
* **windows:** event name not matches with SPEC ([#4455](https://github.com/TheWidlarzGroup/react-native-video/issues/4455)) ([fa20223](https://github.com/TheWidlarzGroup/react-native-video/commit/fa20223c4498e72bd13b87b05c0297b544c4724b))
### Features
* enhance react-native-video plugins [Plugins API Breaking] ([#4366](https://github.com/TheWidlarzGroup/react-native-video/issues/4366)) ([6e6f915](https://github.com/TheWidlarzGroup/react-native-video/commit/6e6f91517c492cdc7d2140ae8564712e9a98450a))
* **windows:** add topSeek parms mentioned in docs ([#4456](https://github.com/TheWidlarzGroup/react-native-video/issues/4456)) ([d902c1b](https://github.com/TheWidlarzGroup/react-native-video/commit/d902c1bf4390a5503eb0ff924ec7cafb2b08a428))
## [6.10.2](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.10.1...v6.10.2) (2025-02-22)
### Bug Fixes
* **android:** fix gradle exception text related to AndroidX version ([#4420](https://github.com/TheWidlarzGroup/react-native-video/issues/4420)) ([6697cbf](https://github.com/TheWidlarzGroup/react-native-video/commit/6697cbf5d07ce5cc882637acc1249192f6189e53))
* **tvOS:** fix tvos compile error for rotation handler ([#4417](https://github.com/TheWidlarzGroup/react-native-video/issues/4417)) ([04eec42](https://github.com/TheWidlarzGroup/react-native-video/commit/04eec42f1e0f8b8f25dc1c50e1e57e62ca8c3356))
## [6.10.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.10.0...v6.10.1) (2025-02-15)
### Bug Fixes
* **android:** check androidX version at early build process before really launching build ([#4388](https://github.com/TheWidlarzGroup/react-native-video/issues/4388)) ([638f454](https://github.com/TheWidlarzGroup/react-native-video/commit/638f454a2118873de23a14f673848f14eeb10122))
* **android:** check for valid width and height on video format data ([#4394](https://github.com/TheWidlarzGroup/react-native-video/issues/4394)) ([ad52668](https://github.com/TheWidlarzGroup/react-native-video/commit/ad52668d0541a2d29b1ee8087f9d25cc6a5e8ab2))
* **android:** remove transparent black overlay on android default controls ([#4392](https://github.com/TheWidlarzGroup/react-native-video/issues/4392)) ([74b1d5b](https://github.com/TheWidlarzGroup/react-native-video/commit/74b1d5b540a062371689f619f1843807ab7ca0e5))
* **infra:** kotlin linter github action ([#4408](https://github.com/TheWidlarzGroup/react-native-video/issues/4408)) ([2905b61](https://github.com/TheWidlarzGroup/react-native-video/commit/2905b61a0d993715cdf94d9f85f949c9e64d098b))
* **ios:** fix constraints when controls are enabled and video is inside a ScrollView ([#4383](https://github.com/TheWidlarzGroup/react-native-video/issues/4383)) ([a8ca97f](https://github.com/TheWidlarzGroup/react-native-video/commit/a8ca97f05fd52ec331fd8fe0e7c2375956a76c2c))
* **ios:** the video has no audio by default ([#4409](https://github.com/TheWidlarzGroup/react-native-video/issues/4409)) ([c8b800a](https://github.com/TheWidlarzGroup/react-native-video/commit/c8b800a508f51a4488b7b260a6bf573b5b6d44d9)), closes [#4400](https://github.com/TheWidlarzGroup/react-native-video/issues/4400)
* **sample:** remove duplicate code in sample ([#4391](https://github.com/TheWidlarzGroup/react-native-video/issues/4391)) ([faac5ad](https://github.com/TheWidlarzGroup/react-native-video/commit/faac5ad45689a605045dae57061d6648244c7dad))
# [6.10.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.9.1...v6.10.0) (2025-01-22)
### Bug Fixes
* **android:** support RN 0.77 ([#4386](https://github.com/TheWidlarzGroup/react-native-video/issues/4386)) ([8b952e7](https://github.com/TheWidlarzGroup/react-native-video/commit/8b952e709a6535325ffbfab9358538d06b308a15))
* **ios:** fix paused video by default ([#4379](https://github.com/TheWidlarzGroup/react-native-video/issues/4379)) ([3d3eba9](https://github.com/TheWidlarzGroup/react-native-video/commit/3d3eba97e814519cd79836208af25b948059795b))
* **tvOS:** fix tvos compile error ([#4369](https://github.com/TheWidlarzGroup/react-native-video/issues/4369)) ([6c3af99](https://github.com/TheWidlarzGroup/react-native-video/commit/6c3af99979c847887796baefd421cb5a50ed32ba))
* **windows:** conversion of string to Stretch enum ([#4381](https://github.com/TheWidlarzGroup/react-native-video/issues/4381)) ([d90bf47](https://github.com/TheWidlarzGroup/react-native-video/commit/d90bf47df58e61a8946f4762659f3e5613aba95d))
### Features
* **web:** implement web pip method and event ([#4370](https://github.com/TheWidlarzGroup/react-native-video/issues/4370)) ([8dc10fd](https://github.com/TheWidlarzGroup/react-native-video/commit/8dc10fd4b774b2cc91bf75f4f1680b63411b8559))
## [6.9.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.9.0...v6.9.1) (2025-01-10)
### Bug Fixes
* avoid memory leak on iOS ([#4355](https://github.com/TheWidlarzGroup/react-native-video/issues/4355)) ([424f4ee](https://github.com/TheWidlarzGroup/react-native-video/commit/424f4eeddea989392e25c52f45a9a0281ead6fe1))
* NPE in setEnterPictureInPictureOnLeave for unsupported Android versions ([#4362](https://github.com/TheWidlarzGroup/react-native-video/issues/4362)) ([3924b5e](https://github.com/TheWidlarzGroup/react-native-video/commit/3924b5e295ed64c97284f4665bc294066a83574a))
# [6.9.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.8.2...v6.9.0) (2025-01-04)
### Bug Fixes
* **android:** disable caching on local asset files ([#4304](https://github.com/TheWidlarzGroup/react-native-video/issues/4304)) ([63c592f](https://github.com/TheWidlarzGroup/react-native-video/commit/63c592f7cd897caf918fd3bd5f129c72432d2b55))
* **docs:** bump `next.js` version & fix meta warnings ([#4327](https://github.com/TheWidlarzGroup/react-native-video/issues/4327)) ([7b4bd9a](https://github.com/TheWidlarzGroup/react-native-video/commit/7b4bd9a0169fc2ea6f277dd7ed904bada98bc63a))
* hiding poster ([#4308](https://github.com/TheWidlarzGroup/react-native-video/issues/4308)) ([621a802](https://github.com/TheWidlarzGroup/react-native-video/commit/621a80299c690c07846f3fcd8a6c73b7ecde39bf))
* **ios:** `_paused` is updated when video playback pause ([#4320](https://github.com/TheWidlarzGroup/react-native-video/issues/4320)) ([3da4f1c](https://github.com/TheWidlarzGroup/react-native-video/commit/3da4f1ca979058b387b1be2c2141f6b93fd084a7))
* **ios:** disables subtitles for `none` and `empty` track types ([#4319](https://github.com/TheWidlarzGroup/react-native-video/issues/4319)) ([1033c9d](https://github.com/TheWidlarzGroup/react-native-video/commit/1033c9d4f3db7042a96e7108a7fe9f1567d69ded))
### Features
* implement enterPictureInPictureOnLeave prop for both platform (Android, iOS) ([#3385](https://github.com/TheWidlarzGroup/react-native-video/issues/3385)) ([69a7bc2](https://github.com/TheWidlarzGroup/react-native-video/commit/69a7bc2d265f2cf4985f8d81054c46f47ee3bae2))
## [6.8.2](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.8.1...v6.8.2) (2024-11-25)
### Bug Fixes
* playback restart without bufferingConfig ([#4305](https://github.com/TheWidlarzGroup/react-native-video/issues/4305)) ([f37dc9e](https://github.com/TheWidlarzGroup/react-native-video/commit/f37dc9e33ebefd922605c5ae91360379fe91bed6))
## [6.8.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.8.0...v6.8.1) (2024-11-24)
### Bug Fixes
* **ios:** handle async player access in text track selection ([#4293](https://github.com/TheWidlarzGroup/react-native-video/issues/4293)) ([daaac97](https://github.com/TheWidlarzGroup/react-native-video/commit/daaac9740aed1858b7ababae0ec8b08274130a27))
# [6.8.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.7.0...v6.8.0) (2024-11-17)
### Bug Fixes
* **android:** add helper to avoid type error ([#4257](https://github.com/TheWidlarzGroup/react-native-video/issues/4257)) ([3b4bfd3](https://github.com/TheWidlarzGroup/react-native-video/commit/3b4bfd3936a8cb846c0e61ffd396940987a7ba43))
# [6.7.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.6.4...v6.7.0) (2024-10-17)
### Bug Fixes
* **android:** sideloaded subtitles ([#4232](https://github.com/TheWidlarzGroup/react-native-video/issues/4232)) ([352dfbb](https://github.com/TheWidlarzGroup/react-native-video/commit/352dfbbc9bef158400c59516a50d889d25757c0d))
* ensure aspect ratio from video is handled in a coherent way ([#4219](https://github.com/TheWidlarzGroup/react-native-video/issues/4219)) ([a8d5841](https://github.com/TheWidlarzGroup/react-native-video/commit/a8d5841c7c0f9767ec095ffd8401b1579f32623f))
* **iOS:** pause video on end reached & don't remove listeners ([#4218](https://github.com/TheWidlarzGroup/react-native-video/issues/4218)) ([2c19a47](https://github.com/TheWidlarzGroup/react-native-video/commit/2c19a4770df73179436a9e23a5e55ad0699fcfcc))
* remove warning and refactor & fix ad workflow ([#4235](https://github.com/TheWidlarzGroup/react-native-video/issues/4235)) ([7501880](https://github.com/TheWidlarzGroup/react-native-video/commit/7501880062a4c381838949084f0017a2aecc58d7))
### Features
* add setSource API function fix ads playback ([#4185](https://github.com/TheWidlarzGroup/react-native-video/issues/4185)) ([9a3fcda](https://github.com/TheWidlarzGroup/react-native-video/commit/9a3fcda3b8ca4689c9131a12a8375fc43d442f80))
* **android:** add settings button to control video playback speed ([#4211](https://github.com/TheWidlarzGroup/react-native-video/issues/4211)) ([d1883a7](https://github.com/TheWidlarzGroup/react-native-video/commit/d1883a7e008706cac2a2beac934194539c9b5b77))
* **exoplayerview:** Migrate ExoPlayerView to kotlin ([#4038](https://github.com/TheWidlarzGroup/react-native-video/issues/4038)) ([78f4f04](https://github.com/TheWidlarzGroup/react-native-video/commit/78f4f0480d70d209fea9e0579963e347c965fd6e))
## [6.6.4](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.6.3...v6.6.4) (2024-10-03)
@@ -514,7 +772,7 @@
* add release-it ([#3342](https://github.com/react-native-video/react-native-video/issues/3342)) ([da27089](https://github.com/react-native-video/react-native-video/commit/da270891fbce485bb132825a336638f2af98408d))
* **ios:** add onBandwidthUpdate event ([#3331](https://github.com/react-native-video/react-native-video/issues/3331)) ([9054db3](https://github.com/react-native-video/react-native-video/commit/9054db35d7d5e4e6d54739fc9349576c03522d7c))
## Changelog
#
## Next
- Android, iOS: add onVolumeChange event #3322

View File

@@ -24,8 +24,30 @@ For example, a single file JS code change requires 1 review while a 3 files iOS
* If you have time to help out, look for the [`review requested`](https://github.com/TheWidlarzGroup/react-native-video/labels/review%20requested) label. It will have another numeric label with it (`1`, `2`, or `3` indicating how many more reviews are needed to merge).
## Releases
* Aim for a bi-weekly (every other week) release to flush out whatever was approved and merge. Most people use this with a lock file (and if you don't you are doing it wrong) and should not have any issues with new bugs showing up. This is already a high risk dependency which must be tested well before going into production. Let's take advantage of that and move faster.
Please do not harass people to review your pull request! You can tag those you feel have relevant experience but please don't abuse this as people will unfollow or mute the project if they are called too many times!
### Running the example
To see how to run examples locally, please refer to the [examples guide](https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples)
### Working on documentation
The documentation is located in the `docs` folder. To work on the documentation, you can run the following command to start a local server:
```sh
cd docs
bun install
bun dev
```
### Publishing a release
We use [release-it](https://github.com/webpro/release-it) to automate our release.
## Reporting issues
You can report issues on our [bug tracker](https://github.com/TheWidlarzGroup/react-native-video/issues). Please follow the issue template when opening an issue.
## License
By contributing to React Native Video, you agree that your contributions will be licensed under its **MIT** license.

211
README.md
View File

@@ -1,62 +1,173 @@
# react-native-video
🎬 `<Video>` component for React Native
[![React Native Video Component](./docs/assets/baners/rnv-banner.png)](https://thewidlarzgroup.com/?utm_source=rnv&utm_medium=readme&utm_id=banner)
## Documentation
documentation is available at [thewidlarzgroup.github.io/react-native-video/](https://thewidlarzgroup.github.io/react-native-video/)
The most battle-tested open-source video player component for React Native with support for DRM, offline playback, HLS/DASH streaming, and more.
## Usage
```javascript
// Load the module
## 🔍 Features
import Video, {VideoRef} from 'react-native-video';
- 📱 Plays all video formats natively supported by iOS/Android
- ▶️ Local and remote playback
- 🔁 Streaming: HLS • DASH • SmoothStreaming
- 🔐 DRM: Widevine & FairPlay ([See free DRM stream example](https://www.thewidlarzgroup.com/services/free-drm-token-generator-for-video?utm_source=rnv&utm_medium=readme&utm_id=free-drm))
- 📴 Offline playback, video download, support for side-tracks and side-captions (via [optional SDK](https://docs.thewidlarzgroup.com/offline-video-sdk?utm_source=rnv&utm_medium=readme&utm_id=features-text))
- 🎚️ Fine-grained control over tracks, buffering & events
- 🧩 Expo plugin support
- 🌐 Basic Web Support
- 📱 Picture in Picture
- 📺 TV Support
// Within your render function, assuming you have a file called
// "background.mp4" in your project. You can include multiple videos
// on a single screen if you like.
const VideoPlayer = () => {
const videoRef = useRef<VideoRef>(null);
const background = require('./background.mp4');
return (
<Video
// Can be a URL or a local file.
source={background}
// Store reference
ref={videoRef}
// Callback when remote video is buffering
onBuffer={onBuffer}
// Callback when video cannot be loaded
onError={onError}
style={styles.backgroundVideo}
/>
)
}
## ✨ Project Status
// Later on in your styles..
var styles = StyleSheet.create({
backgroundVideo: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
},
});
| Version | State | Architecture |
|---------|-------|--------------|
| **v5 and lower** | ❌ End-of-life [Commercial Support Available](https://www.thewidlarzgroup.com/blog/react-native-video-upgrade-challenges-custom-maintenance-support#how-we-can-help?utm_source=rnv&utm_medium=readme&utm_id=upgradev5) | Old Architecture |
| **v6** | 🛠 Maintained (community + TWG) | Old + New (Interop Layer) |
| [**v7**](https://github.com/TheWidlarzGroup/react-native-video/tree/v7) | [🚀 Active Development](https://github.com/TheWidlarzGroup/react-native-video/tree/v7) | Old + New (Full Support) |
[`react-native-video` v7](https://github.com/TheWidlarzGroup/react-native-video/tree/v7) introduces full support for the new React Native architecture, unlocking better performance, improved consistency, and modern native modules.
---
## 📚 Documentation & Examples
- 📖 [Documentation](https://docs.thewidlarzgroup.com/react-native-video/)
- 📦 [Example: Free DRM Stream](https://www.thewidlarzgroup.com/services/free-drm-token-generator-for-video?utm_source=rnv&utm_medium=readme&utm_id=free-drm)
- 📦 [Example: Offline SDK integration](https://docs.thewidlarzgroup.com/offline-video-sdk)
## 🚀 Quick Start
### Install
```bash
# Install dependencies
yarn add react-native-video
# Install pods
cd ios && pod install
```
## Community support
We have an discord server where you can ask questions and get help. [Join the discord server](https://discord.gg/WXuM4Tgb9X)
### Usage
```tsx
import Video from 'react-native-video';
## Enterprise Support
<p>
📱 <i>react-native-video</i> is provided <i>as it is</i>. For enterprise support or other business inquiries, <a href="https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=readme#Contact">please contact us 🤝</a>. We can help you with the integration, customization and maintenance. We are providing both free and commercial support for this project. let's build something awesome together! 🚀
</p>
<a href="https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=readme">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./docs/assets/baners/twg-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./docs/assets/baners/twg-light.png" />
<img alt="TheWidlarzGroup" src="./docs/assets/baners/twg-light.png" />
</picture>
export default () => (
<Video
source={{ uri: 'https://www.w3schools.com/html/mov_bbb.mp4' }}
style={{ width: '100%', aspectRatio: 16 / 9 }}
controls
/>
);
```
---
## 🧩 Plugins
<a href="https://sdk.thewidlarzgroup.com/offline-video?utm_source=rnv&utm_medium=readme&utm_id=banner">
<img src="./docs/assets/baners/offline-sdk-banner.png" alt="Offline SDK Preview" width="40%" align="right" />
</a>
### 1 · 📥 Offline SDK
#### Need Offline Video Playback in React Native?
If you're building a video-first app and need to **download HLS streams for offline playback**, you're in the right place.
#### 👉 [Check Offline Video SDK for React Native](https://sdk.thewidlarzgroup.com/offline-video?utm_source=rnv&utm_medium=readme&utm_id=check-offline-video-sdk)
This SDK supports:
- 🎞 Offline HLS playback
- 🎧 Multi-language audio track downloads
- 💬 Subtitles support
- 🔐 DRM license handling
- 📊 Analytics & state tracking
---
#### 🔑 How to get access?
- Get a **free trial** (no credit card required)
- Use our [starter project](https://github.com/TheWidlarzGroup/react-native-offline-video-starter) to see it in action
- Integrates with both `v6` and `v7` versions
👉 **[Start Free Trial on the SDK Platform →](https://sdk.thewidlarzgroup.com/signup?utm_source=rnv&utm_medium=readme&utm_id=start-trial-offline-video-sdk)**
---
<a href="https://sdk.thewidlarzgroup.com/background-uploader?utm_source=rnv&utm_medium=readme&utm_id=banner">
<img src="./docs/assets/baners/bgupload-sdk-banner.png" alt="Offline SDK Preview" width="40%" align="right" />
</a>
### 2 · ⚡ Background Upload SDK
#### Need Reliable Video Uploads in React Native?
If you're building a video-first app and need to **upload large video files reliably in the background**, you're in the right place.
#### 👉 [Check Background Upload SDK for React Native](https://sdk.thewidlarzgroup.com/background-uploader?utm_source=rnv&utm_medium=readme&utm_id=check-background-upload-sdk)
This SDK supports:
- 📤 Background video uploads
- 🔄 Automatic retry mechanisms
- 📊 Upload progress tracking
- 🛡️ Resume interrupted uploads
- 📱 Works when app is backgrounded
- 🔐 Secure upload handling
---
#### 🚀 Perfect for Apps Uploading Large Media
Whether you're building social media apps, content platforms, or enterprise solutions, our Background Upload SDK ensures your users can upload videos seamlessly without interruption.
#### 📞 Ready to Get Started?
Contact us to learn more about integrating background video uploads into your React Native application.
👉 **Contact us at [hi@thewidlarzgroup.com](mailto:hi@thewidlarzgroup.com)**
---
### 3 · 🧪 Architecture
Write your own plugins to extend library logic, attach analytics or add custom workflows - **without forking** the core SDK.
→ [Plugin documentation](https://docs.thewidlarzgroup.com/react-native-video/docs/v6/other/plugin?utm_source=rnv&utm_medium=readme&utm_id=plugin-text)
---
## 💼 TWG Services & Products
| Offering | Description |
|----------|-------------|
| [**Professional Support Packages**](https://www.thewidlarzgroup.com/issue-boost?utm_source=rnv&utm_medium=readme&utm_campaign=professional-support-packages#Contact) | Priority bug-fixes, guaranteed SLAs, [roadmap influence](https://github.com/orgs/TheWidlarzGroup/projects/6) |
| [**Issue Booster**](https://www.thewidlarzgroup.com/issue-boost?utm_source=rnv&utm_medium=readme) | Fast-track urgent fixes with a payperissue model |
| [**Offline Video SDK**](https://sdk.thewidlarzgroup.com/offline-video?utm_source=rnv&utm_medium=readme&utm_campaign=downloading&utm_id=offline-video-sdk-link) | Plugandplay secure download solution for iOS & Android |
| [**Background Upload SDK**](https://sdk.thewidlarzgroup.com/background-uploader?utm_source=rnv&utm_medium=readme&utm_campaign=uploading&utm_id=background-upload-sdk-link) | Reliable background upload solution for iOS & Android |
| [**Integration Support**](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=readme&utm_campaign=integration-support#Contact) | Handson help integrating video, DRM & offline into your app |
| [**Free DRM Token Generator**](https://www.thewidlarzgroup.com/services/free-drm-token-generator-for-video?utm_source=rnv&utm_medium=readme&utm_id=free-drm) | Generate Widevine / FairPlay tokens for testing |
| [**Ready Boilerplates**](https://www.thewidlarzgroup.com/showcases?utm_source=rnv&utm_medium=readme) | Ready-to-use apps with offline HLS/DASH DRM, video frame scrubbing, TikTok-style video feed, background uploads, Skia-based frame processor (R&D phase), and more |
| [**React Native Video Upgrade Guide**](https://www.thewidlarzgroup.com/blog/react-native-video-upgrade-challenges-custom-maintenance-support?utm_source=rnv&utm_medium=readme&utm_id=upgrade-blog&utm_campaign=v7) | Common upgrade pitfalls & how to solve them |
*See how [TWG](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=readme&utm_id=services-text) helped **Learnn** ship a worldclass player in record time - [case study](https://gitnation.com/contents/a-4-year-retrospective-lessons-learned-from-building-a-video-player-from-scratch-with-react-native).*
Contact us at [hi@thewidlarzgroup.com](mailto:hi@thewidlarzgroup.com)
## 🌍 Social
- 🐦 **X / Twitter** - [follow product & release updates](https://x.com/TheWidlarzGroup)
- 💬 **Discord** - [talk to the community and us](https://discord.gg/9WPq6Yx)
- 💼 **LinkedIn** - [see TWG flexing](https://linkedin.com/company/the-widlarz-group)
## 📰 Community & Media
- 🗽 **React Summit US** How TWG helped Learnn boost video performance on React Native.
[Watch the talk »](https://gitnation.com/contents/a-4-year-retrospective-lessons-learned-from-building-a-video-player-from-scratch-with-react-native)
- 🧨 **v7 deep dive** Why were building v7 with Nitro Modules
[Watch on X »](https://x.com/krzysztof_moch/status/1854162551946478051)
- 🛠️ **Well-maintained open-source library** - What does it truly mean? - Bart's talk for React Native Warsaw
[Watch here »](https://www.youtube.com/watch?v=RAQQwGCQNqY)
- 📺 **“Over the Top” Panel** - Building Streaming Apps for Mobile, Web, and Smart TVs - Bart giving his insights on the industry
[Watch here »](https://youtu.be/j2b_bG-32JI)

View File

@@ -7,6 +7,9 @@ buildscript {
def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['RNVideo_kotlinVersion']
def requiredKotlinVersion = project.properties['RNVideo_kotlinVersion']
def androidx_version = rootProject.ext.has('androidxActivityVersion') ? rootProject.ext.get('androidxActivityVersion') : project.properties['RNVideo_androidxActivityVersion']
def requiredAndroidxVersion = project.properties['RNVideo_androidxActivityVersion']
def isVersionAtLeast = { version, requiredVersion ->
def (v1, v2) = [version, requiredVersion].collect { it.tokenize('.')*.toInteger() }
for (int i = 0; i < Math.max(v1.size(), v2.size()); i++) {
@@ -35,6 +38,11 @@ buildscript {
} else {
println("Kotlin version is correct: $kotlin_version")
}
if (!isVersionAtLeast(androidx_version, requiredAndroidxVersion)) {
throw new GradleException("AndroidX version mismatch: Project is using AndroidX version $androidx_version, but it must be at least $requiredAndroidxVersion. Please update the AndroidX version.")
} else {
println("AndroidX version is correct: $androidx_version")
}
}
}
@@ -91,9 +99,10 @@ def configStringPath = ExoplayerDependencies
.concat("buildFromSource:$media3_buildFromSource")
.md5()
if (isNewArchitectureEnabled()) {
apply plugin: "com.facebook.react"
}
// commented as new architecture not yet fully supported
// if (isNewArchitectureEnabled()) {
// apply plugin: "com.facebook.react"
// }
android {
if (supportsNamespace()) {
@@ -133,6 +142,11 @@ android {
buildConfigField "boolean", "USE_EXOPLAYER_DASH", ExoplayerDependencies["useExoplayerDash"].toString()
buildConfigField "boolean", "USE_EXOPLAYER_HLS", ExoplayerDependencies["useExoplayerHls"].toString()
buildConfigField "boolean", "USE_EXOPLAYER_RTSP", ExoplayerDependencies["useExoplayerRtsp"].toString()
externalNativeBuild {
cmake {
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
}
}
ndk {
abiFilters(*reactNativeArchitectures())

View File

@@ -1,15 +1,15 @@
RNVideo_kotlinVersion=1.8.0
RNVideo_minSdkVersion=23
RNVideo_targetSdkVersion=34
RNVideo_compileSdkVersion=34
RNVideo_ndkversion=26.1.10909125
RNVideo_buildToolsVersion=34.0.0
RNVideo_media3Version=1.4.1
RNVideo_minSdkVersion=24
RNVideo_targetSdkVersion=35
RNVideo_compileSdkVersion=35
RNVideo_ndkversion=27.1.12297006
RNVideo_buildToolsVersion=35.0.0
RNVideo_media3Version=1.8.0
RNVideo_useExoplayerIMA=false
RNVideo_useExoplayerRtsp=false
RNVideo_useExoplayerSmoothStreaming=true
RNVideo_useExoplayerDash=true
RNVideo_useExoplayerHls=true
RNVideo_androidxCoreVersion=1.13.1
RNVideo_androidxActivityVersion=1.8.2
RNVideo_androidxActivityVersion=1.9.3
RNVideo_buildFromMedia3Source=false

View File

@@ -0,0 +1,65 @@
package androidx.media3.exoplayer.ima;
import android.content.Context;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
public class ImaServerSideAdInsertionMediaSource {
public static class AdsLoader {
public void setPlayer(@Nullable Player player) {
}
public void release() {
}
public static class Builder {
public Builder(Context context, View playerView) {
}
public Builder setAdEventListener(Object listener) {
return this;
}
public Builder setAdErrorListener(Object listener) {
return this;
}
public AdsLoader build() {
return new AdsLoader();
}
}
}
public static class Factory implements MediaSource.Factory {
public Factory(AdsLoader adsLoader, MediaSource.Factory mediaSourceFactory) {
}
@Override
public MediaSource.Factory setDrmSessionManagerProvider(DrmSessionManagerProvider drmSessionManagerProvider) {
return this;
}
@Override
public MediaSource.Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
return this;
}
@Override
public int[] getSupportedTypes() {
return new int[0];
}
@Override
public MediaSource createMediaSource(MediaItem mediaItem) {
return null;
}
}
}

View File

@@ -0,0 +1,26 @@
package androidx.media3.exoplayer.ima;
import android.net.Uri;
public class ImaServerSideAdInsertionUriBuilder {
public ImaServerSideAdInsertionUriBuilder setAssetKey(String assetKey) {
return this;
}
public ImaServerSideAdInsertionUriBuilder setContentSourceId(String contentSourceId) {
return this;
}
public ImaServerSideAdInsertionUriBuilder setVideoId(String videoId) {
return this;
}
public ImaServerSideAdInsertionUriBuilder setFormat(int format) {
return this;
}
public Uri build() {
return Uri.EMPTY;
}
}

View File

@@ -2,43 +2,100 @@ package com.brentvatne.common.api
import android.net.Uri
import android.text.TextUtils
import com.brentvatne.common.toolbox.DebugLog
import com.brentvatne.common.toolbox.ReactBridgeUtils
import com.facebook.react.bridge.ReadableMap
import java.util.Objects
class AdsProps {
var type: String? = null
var streamType: String? = null
var adTagUrl: Uri? = null
var adLanguage: String? = null
var contentSourceId: String? = null
var videoId: String? = null
var assetKey: String? = null
var format: String? = null
var adTagParameters: Map<String, String>? = null
var fallbackUri: String? = null
fun isCSAI(): Boolean = type == "csai" && adTagUrl != null
fun isDAI(): Boolean = type == "ssai"
fun isDAIVod(): Boolean = type == "ssai" && streamType == "vod"
fun isDAILive(): Boolean = type == "ssai" && streamType == "live"
/** return true if this and src are equals */
override fun equals(other: Any?): Boolean {
if (other == null || other !is AdsProps) return false
return (
type == other.type &&
streamType == other.streamType &&
adTagUrl == other.adTagUrl &&
adLanguage == other.adLanguage
adLanguage == other.adLanguage &&
contentSourceId == other.contentSourceId &&
videoId == other.videoId &&
assetKey == other.assetKey &&
format == other.format &&
adTagParameters == other.adTagParameters &&
fallbackUri == other.fallbackUri
)
}
override fun hashCode(): Int =
Objects.hash(
type, streamType, adTagUrl, adLanguage, contentSourceId, videoId, assetKey, format, adTagParameters, fallbackUri
)
companion object {
private const val PROP_TYPE = "type"
private const val PROP_STREAM_TYPE = "streamType"
private const val PROP_AD_TAG_URL = "adTagUrl"
private const val PROP_AD_LANGUAGE = "adLanguage"
private const val PROP_CONTENT_SOURCE_ID = "contentSourceId"
private const val PROP_VIDEO_ID = "videoId"
private const val PROP_ASSET_KEY = "assetKey"
private const val PROP_FORMAT = "format"
private const val PROP_AD_TAG_PARAMETERS = "adTagParameters"
private const val PROP_FALLBACK_URI = "fallbackUri"
@JvmStatic
fun parse(src: ReadableMap?): AdsProps {
val adsProps = AdsProps()
DebugLog.w("olivier", "uri: parse AdsProps")
if (src != null) {
adsProps.type = ReactBridgeUtils.safeGetString(src, PROP_TYPE)
adsProps.streamType = ReactBridgeUtils.safeGetString(src, PROP_STREAM_TYPE)
val uriString = ReactBridgeUtils.safeGetString(src, PROP_AD_TAG_URL)
if (TextUtils.isEmpty(uriString)) {
adsProps.adTagUrl = null
} else {
if (!TextUtils.isEmpty(uriString)) {
adsProps.adTagUrl = Uri.parse(uriString)
}
val languageString = ReactBridgeUtils.safeGetString(src, PROP_AD_LANGUAGE)
if (!TextUtils.isEmpty(languageString)) {
adsProps.adLanguage = languageString
}
adsProps.contentSourceId = ReactBridgeUtils.safeGetString(src, PROP_CONTENT_SOURCE_ID)
adsProps.videoId = ReactBridgeUtils.safeGetString(src, PROP_VIDEO_ID)
adsProps.assetKey = ReactBridgeUtils.safeGetString(src, PROP_ASSET_KEY)
adsProps.format = ReactBridgeUtils.safeGetString(src, PROP_FORMAT)
adsProps.fallbackUri = ReactBridgeUtils.safeGetString(src, PROP_FALLBACK_URI)
if (src.hasKey(PROP_AD_TAG_PARAMETERS)) {
val adTagParamsMap = src.getMap(PROP_AD_TAG_PARAMETERS)
if (adTagParamsMap != null) {
val params = mutableMapOf<String, String>()
val iterator = adTagParamsMap.keySetIterator()
while (iterator.hasNextKey()) {
val key = iterator.nextKey()
val value = adTagParamsMap.getString(key)
if (value != null) {
params[key] = value
}
}
if (params.isNotEmpty()) {
adsProps.adTagParameters = params
}
}
}
}
return adsProps
}

View File

@@ -20,9 +20,28 @@ class BufferConfig {
var maxHeapAllocationPercent = BufferConfigPropUnsetDouble
var minBackBufferMemoryReservePercent = BufferConfigPropUnsetDouble
var minBufferMemoryReservePercent = BufferConfigPropUnsetDouble
var initialBitrate = BufferConfigPropUnsetInt
var live: Live = Live()
/** return true if this and src are equals */
override fun equals(other: Any?): Boolean {
if (other == null || other !is BufferConfig) return false
return (
cacheSize == other.cacheSize &&
minBufferMs == other.minBufferMs &&
maxBufferMs == other.maxBufferMs &&
bufferForPlaybackMs == other.bufferForPlaybackMs &&
bufferForPlaybackAfterRebufferMs == other.bufferForPlaybackAfterRebufferMs &&
backBufferDurationMs == other.backBufferDurationMs &&
maxHeapAllocationPercent == other.maxHeapAllocationPercent &&
minBackBufferMemoryReservePercent == other.minBackBufferMemoryReservePercent &&
minBufferMemoryReservePercent == other.minBufferMemoryReservePercent &&
initialBitrate == other.initialBitrate &&
live == other.live
)
}
class Live {
var maxPlaybackSpeed: Float = BufferConfigPropUnsetDouble.toFloat()
var minPlaybackSpeed: Float = BufferConfigPropUnsetDouble.toFloat()
@@ -30,12 +49,23 @@ class BufferConfig {
var minOffsetMs: Long = BufferConfigPropUnsetInt.toLong()
var targetOffsetMs: Long = BufferConfigPropUnsetInt.toLong()
override fun equals(other: Any?): Boolean {
if (other == null || other !is Live) return false
return (
maxPlaybackSpeed == other.maxPlaybackSpeed &&
minPlaybackSpeed == other.minPlaybackSpeed &&
maxOffsetMs == other.maxOffsetMs &&
minOffsetMs == other.minOffsetMs &&
targetOffsetMs == other.targetOffsetMs
)
}
companion object {
private val PROP_BUFFER_CONFIG_LIVE_MAX_PLAYBACK_SPEED = "maxPlaybackSpeed"
private val PROP_BUFFER_CONFIG_LIVE_MIN_PLAYBACK_SPEED = "minPlaybackSpeed"
private val PROP_BUFFER_CONFIG_LIVE_MAX_OFFSET_MS = "maxOffsetMs"
private val PROP_BUFFER_CONFIG_LIVE_MIN_OFFSET_MS = "minOffsetMs"
private val PROP_BUFFER_CONFIG_LIVE_TARGET_OFFSET_MS = "targetOffsetMs"
private const val PROP_BUFFER_CONFIG_LIVE_MAX_PLAYBACK_SPEED = "maxPlaybackSpeed"
private const val PROP_BUFFER_CONFIG_LIVE_MIN_PLAYBACK_SPEED = "minPlaybackSpeed"
private const val PROP_BUFFER_CONFIG_LIVE_MAX_OFFSET_MS = "maxOffsetMs"
private const val PROP_BUFFER_CONFIG_LIVE_MIN_OFFSET_MS = "minOffsetMs"
private const val PROP_BUFFER_CONFIG_LIVE_TARGET_OFFSET_MS = "targetOffsetMs"
@JvmStatic
fun parse(src: ReadableMap?): Live {
@@ -54,16 +84,17 @@ class BufferConfig {
val BufferConfigPropUnsetInt = -1
val BufferConfigPropUnsetDouble = -1.0
private val PROP_BUFFER_CONFIG_CACHE_SIZE = "cacheSizeMB"
private val PROP_BUFFER_CONFIG_MIN_BUFFER_MS = "minBufferMs"
private val PROP_BUFFER_CONFIG_MAX_BUFFER_MS = "maxBufferMs"
private val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs"
private val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = "bufferForPlaybackAfterRebufferMs"
private val PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT = "maxHeapAllocationPercent"
private val PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT = "minBackBufferMemoryReservePercent"
private val PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT = "minBufferMemoryReservePercent"
private val PROP_BUFFER_CONFIG_BACK_BUFFER_DURATION_MS = "backBufferDurationMs"
private val PROP_BUFFER_CONFIG_LIVE = "live"
private const val PROP_BUFFER_CONFIG_CACHE_SIZE = "cacheSizeMB"
private const val PROP_BUFFER_CONFIG_MIN_BUFFER_MS = "minBufferMs"
private const val PROP_BUFFER_CONFIG_MAX_BUFFER_MS = "maxBufferMs"
private const val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs"
private const val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = "bufferForPlaybackAfterRebufferMs"
private const val PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT = "maxHeapAllocationPercent"
private const val PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT = "minBackBufferMemoryReservePercent"
private const val PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT = "minBufferMemoryReservePercent"
private const val PROP_BUFFER_CONFIG_BACK_BUFFER_DURATION_MS = "backBufferDurationMs"
private const val PROP_BUFFER_CONFIG_INITIAL_BITRATE = "initialBitrate"
private const val PROP_BUFFER_CONFIG_LIVE = "live"
@JvmStatic
fun parse(src: ReadableMap?): BufferConfig {
@@ -90,6 +121,7 @@ class BufferConfig {
BufferConfigPropUnsetDouble
)
bufferConfig.backBufferDurationMs = safeGetInt(src, PROP_BUFFER_CONFIG_BACK_BUFFER_DURATION_MS, BufferConfigPropUnsetInt)
bufferConfig.initialBitrate = safeGetInt(src, PROP_BUFFER_CONFIG_INITIAL_BITRATE, BufferConfigPropUnsetInt)
bufferConfig.live = Live.parse(src.getMap(PROP_BUFFER_CONFIG_LIVE))
}
return bufferConfig

View File

@@ -39,8 +39,8 @@ data class CMCDProps(
val item = array.getMap(i)
val key = item?.getString("key")
val value = when (item?.getType("value")) {
ReadableType.Number -> item.getDouble("value")
ReadableType.String -> item.getString("value")
ReadableType.Number -> item?.getDouble("value")
ReadableType.String -> item?.getString("value")
else -> null
}

View File

@@ -24,8 +24,10 @@ class SideLoadedTextTrackList {
}
val sideLoadedTextTrackList = SideLoadedTextTrackList()
for (i in 0 until src.size()) {
val textTrack: ReadableMap = src.getMap(i)
sideLoadedTextTrackList.tracks.add(SideLoadedTextTrack.parse(textTrack))
val textTrack: ReadableMap? = src.getMap(i)
textTrack?.let {
sideLoadedTextTrackList.tracks.add(SideLoadedTextTrack.parse(it))
}
}
return sideLoadedTextTrackList
}

View File

@@ -5,7 +5,6 @@ import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources
import android.net.Uri
import android.text.TextUtils
import com.brentvatne.common.api.DRMProps.Companion.parse
import com.brentvatne.common.toolbox.DebugLog
import com.brentvatne.common.toolbox.DebugLog.e
@@ -30,6 +29,12 @@ class Source {
/** Parsed value of source to playback */
var uri: Uri? = null
/** True if source is a local JS asset */
var isLocalAssetFile: Boolean = false
/** True if source is a local file asset://, ... */
var isAsset: Boolean = false
/** Start position of playback used to resume playback */
var startPositionMs: Int = -1
@@ -48,6 +53,9 @@ class Source {
/** Metadata to display in notification */
var metadata: Metadata? = null
/** Allowed reload before failure notification */
var minLoadRetryCount = 3
/** http header list */
val headers: MutableMap<String, String> = HashMap()
@@ -71,12 +79,17 @@ class Source {
*/
var adsProps: AdsProps? = null
/*
* buffering configuration
*/
var bufferConfig = BufferConfig()
/**
* The list of sideLoaded text tracks
*/
var sideLoadedTextTracks: SideLoadedTextTrackList? = null
override fun hashCode(): Int = Objects.hash(uriString, uri, startPositionMs, cropStartMs, cropEndMs, extension, metadata, headers)
override fun hashCode(): Int = Objects.hash(uriString, uri, startPositionMs, cropStartMs, cropEndMs, extension, metadata, headers, adsProps)
/** return true if this and src are equals */
override fun equals(other: Any?): Boolean {
@@ -91,7 +104,11 @@ class Source {
contentStartTime == other.contentStartTime &&
cmcdProps == other.cmcdProps &&
sideLoadedTextTracks == other.sideLoadedTextTracks &&
adsProps == other.adsProps
adsProps == other.adsProps &&
minLoadRetryCount == other.minLoadRetryCount &&
isLocalAssetFile == other.isLocalAssetFile &&
isAsset == other.isAsset &&
bufferConfig == other.bufferConfig
)
}
@@ -147,6 +164,8 @@ class Source {
companion object {
private const val TAG = "Source"
private const val PROP_SRC_URI = "uri"
private const val PROP_SRC_IS_LOCAL_ASSET_FILE = "isLocalAssetFile"
private const val PROP_SRC_IS_ASSET = "isAsset"
private const val PROP_SRC_START_POSITION = "startPosition"
private const val PROP_SRC_CROP_START = "cropStart"
private const val PROP_SRC_CROP_END = "cropEnd"
@@ -159,6 +178,8 @@ class Source {
private const val PROP_SRC_ADS = "ad"
private const val PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION = "textTracksAllowChunklessPreparation"
private const val PROP_SRC_TEXT_TRACKS = "textTracks"
private const val PROP_SRC_MIN_LOAD_RETRY_COUNT = "minLoadRetryCount"
private const val PROP_SRC_BUFFER_CONFIG = "bufferConfig"
@SuppressLint("DiscouragedApi")
private fun getUriFromAssetId(context: Context, uriString: String): Uri? {
@@ -190,27 +211,23 @@ class Source {
fun parse(src: ReadableMap?, context: Context): Source {
val source = Source()
if (src != null) {
val uriString = safeGetString(src, PROP_SRC_URI, null)
if (uriString == null || TextUtils.isEmpty(uriString)) {
DebugLog.d(TAG, "isEmpty uri:$uriString")
return source
}
if (src == null) return source
safeGetString(src, PROP_SRC_URI, null)
?.takeIf { it.isNotBlank() }
?.let { uriString ->
var uri = Uri.parse(uriString)
if (uri == null) {
// return an empty source
DebugLog.d(TAG, "Invalid uri:$uriString")
return source
} else if (!isValidScheme(uri.scheme)) {
uri = getUriFromAssetId(context, uriString)
if (uri == null) {
// cannot find identifier of content
DebugLog.d(TAG, "cannot find identifier")
return source
}
if (!isValidScheme(uri.scheme)) {
uri = getUriFromAssetId(context, uriString) ?: return source
}
source.uriString = uriString
source.uri = uri
}
source.isLocalAssetFile = safeGetBool(src, PROP_SRC_IS_LOCAL_ASSET_FILE, false)
source.isAsset = safeGetBool(src, PROP_SRC_IS_ASSET, false)
source.startPositionMs = safeGetInt(src, PROP_SRC_START_POSITION, -1)
source.cropStartMs = safeGetInt(src, PROP_SRC_CROP_START, -1)
source.cropEndMs = safeGetInt(src, PROP_SRC_CROP_END, -1)
@@ -223,14 +240,16 @@ class Source {
}
source.textTracksAllowChunklessPreparation = safeGetBool(src, PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION, true)
source.sideLoadedTextTracks = SideLoadedTextTrackList.parse(safeGetArray(src, PROP_SRC_TEXT_TRACKS))
source.minLoadRetryCount = safeGetInt(src, PROP_SRC_MIN_LOAD_RETRY_COUNT, 3)
source.bufferConfig = BufferConfig.parse(safeGetMap(src, PROP_SRC_BUFFER_CONFIG))
val propSrcHeadersArray = safeGetArray(src, PROP_SRC_HEADERS)
if (propSrcHeadersArray != null) {
if (propSrcHeadersArray.size() > 0) {
for (i in 0 until propSrcHeadersArray.size()) {
val current = propSrcHeadersArray.getMap(i)
val key = if (current.hasKey("key")) current.getString("key") else null
val value = if (current.hasKey("value")) current.getString("value") else null
val key = current?.getString("key")
val value = current?.getString("value")
if (key != null && value != null) {
source.headers[key] = value
}
@@ -238,7 +257,7 @@ class Source {
}
}
source.metadata = Metadata.parse(safeGetMap(src, PROP_SRC_METADATA))
}
return source
}

View File

@@ -43,7 +43,8 @@ enum class EventTypes(val eventName: String) {
EVENT_TEXT_TRACK_DATA_CHANGED("onTextTrackDataChanged"),
EVENT_VIDEO_TRACKS("onVideoTracks"),
EVENT_ON_RECEIVE_AD_EVENT("onReceiveAdEvent");
EVENT_ON_RECEIVE_AD_EVENT("onReceiveAdEvent"),
EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED("onPictureInPictureStatusChanged");
companion object {
fun toMap() =
@@ -72,7 +73,7 @@ class VideoEventEmitter {
lateinit var onVideoBandwidthUpdate: (bitRateEstimate: Long, height: Int, width: Int, trackId: String?) -> Unit
lateinit var onVideoPlaybackStateChanged: (isPlaying: Boolean, isSeeking: Boolean) -> Unit
lateinit var onVideoSeek: (currentPosition: Long, seekTime: Long) -> Unit
lateinit var onVideoSeekComplete: (currentPosition: Long) -> Unit
lateinit var onVideoSeekComplete: (currentPosition: Long, seekTime: Long) -> Unit
lateinit var onVideoEnd: () -> Unit
lateinit var onVideoFullscreenPlayerWillPresent: () -> Unit
lateinit var onVideoFullscreenPlayerDidPresent: () -> Unit
@@ -92,6 +93,7 @@ class VideoEventEmitter {
lateinit var onVideoTracks: (videoTracks: ArrayList<VideoTrack>?) -> Unit
lateinit var onTextTrackDataChanged: (textTrackData: String) -> Unit
lateinit var onReceiveAdEvent: (adEvent: String, adData: Map<String?, String?>?) -> Unit
lateinit var onPictureInPictureStatusChanged: (isActive: Boolean) -> Unit
fun addEventEmitters(reactContext: ThemedReactContext, view: ReactExoplayerView) {
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
@@ -140,6 +142,30 @@ class VideoEventEmitter {
putString("errorException", exception.toString())
putString("errorCode", errorCode)
putString("errorStackTrace", stackTrace)
// https://github.com/facebook/react-native/blob/v0.80.2/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp#L465
putMap(
"cause",
Arguments.createMap().apply {
putString("name", exception.javaClass.simpleName)
exception.message?.let { putString("message", it) }
putArray(
"stackElements",
Arguments.createArray().apply {
exception.stackTrace.forEach { element ->
pushMap(
Arguments.createMap().apply {
putString("className", element.className)
putString("fileName", element.fileName)
putInt("lineNumber", element.lineNumber)
putString("methodName", element.methodName)
}
)
}
}
)
}
)
}
)
}
@@ -176,9 +202,11 @@ class VideoEventEmitter {
putDouble("seekTime", seekTime / 1000.0)
}
}
onVideoSeekComplete = { currentPosition ->
onVideoSeekComplete = { currentPosition, seekTime ->
event.dispatch(EventTypes.EVENT_SEEK_COMPLETE) {
putDouble("currentTime", currentPosition / 1000.0)
putDouble("seekTime", seekTime / 1000.0)
putInt("target", view.id)
}
}
onVideoEnd = {
@@ -285,15 +313,25 @@ class VideoEventEmitter {
)
}
}
onPictureInPictureStatusChanged = { isActive ->
event.dispatch(EventTypes.EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED) {
putBoolean("isActive", isActive)
}
}
}
}
private class VideoCustomEvent(surfaceId: Int, viewId: Int, private val event: EventTypes, private val paramsSetter: (WritableMap.() -> Unit)?) :
Event<VideoCustomEvent>(surfaceId, viewId) {
override fun getEventName(): String = "top${event.eventName.removePrefix("on")}"
override fun getEventData(): WritableMap? = Arguments.createMap().apply(paramsSetter ?: {})
}
private class EventBuilder(private val surfaceId: Int, private val viewId: Int, private val dispatcher: EventDispatcher) {
fun dispatch(event: EventTypes, paramsSetter: (WritableMap.() -> Unit)? = null) =
dispatcher.dispatchEvent(object : Event<Event<*>>(surfaceId, viewId) {
override fun getEventName() = "top${event.eventName.removePrefix("on")}"
override fun getEventData() = Arguments.createMap().apply(paramsSetter ?: {})
})
dispatcher.dispatchEvent(VideoCustomEvent(surfaceId, viewId, event, paramsSetter))
}
private fun audioTracksToArray(audioTracks: java.util.ArrayList<Track>?): WritableArray =

View File

@@ -3,6 +3,7 @@ package com.brentvatne.exoplayer
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.upstream.CmcdConfiguration
import com.brentvatne.common.api.CMCDProps
import com.brentvatne.common.toolbox.DebugLog
import com.google.common.collect.ImmutableListMultimap
class CMCDConfig(private val props: CMCDProps) {
@@ -15,9 +16,21 @@ class CMCDConfig(private val props: CMCDProps) {
object : CmcdConfiguration.RequestConfig {
override fun getCustomData(): ImmutableListMultimap<String, String> = buildCustomData()
},
props.mode
intToCmcdMode(props.mode)
)
private fun intToCmcdMode(mode: Int): Int =
when (mode) {
0 -> CmcdConfiguration.MODE_REQUEST_HEADER
1 -> CmcdConfiguration.MODE_QUERY_PARAMETER
else -> {
DebugLog.e("CMCDConfig", "Unsupported mode: $mode, fallback on MODE_REQUEST_HEADER")
CmcdConfiguration.MODE_REQUEST_HEADER
}
}
private fun buildCustomData(): ImmutableListMultimap<String, String> =
ImmutableListMultimap.builder<String, String>().apply {
addFormattedData(this, CmcdConfiguration.KEY_CMCD_OBJECT, props.cmcdObject)

View File

@@ -0,0 +1,59 @@
package com.brentvatne.exoplayer
import androidx.media3.common.util.Util
import androidx.media3.datasource.HttpDataSource
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager
import androidx.media3.exoplayer.drm.DrmSessionManager
import androidx.media3.exoplayer.drm.FrameworkMediaDrm
import androidx.media3.exoplayer.drm.HttpMediaDrmCallback
import androidx.media3.exoplayer.drm.UnsupportedDrmException
import com.brentvatne.common.api.DRMProps
import java.util.UUID
class DRMManager(private val dataSourceFactory: HttpDataSource.Factory) : DRMManagerSpec {
private var hasDrmFailed = false
@Throws(UnsupportedDrmException::class)
override fun buildDrmSessionManager(uuid: UUID, drmProps: DRMProps): DrmSessionManager? = buildDrmSessionManager(uuid, drmProps, 0)
@Throws(UnsupportedDrmException::class)
private fun buildDrmSessionManager(uuid: UUID, drmProps: DRMProps, retryCount: Int = 0): DrmSessionManager? {
if (Util.SDK_INT < 18) {
return null
}
try {
val drmCallback = HttpMediaDrmCallback(drmProps.drmLicenseServer, dataSourceFactory)
// Set DRM headers
val keyRequestPropertiesArray = drmProps.drmLicenseHeader
for (i in keyRequestPropertiesArray.indices step 2) {
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1])
}
val mediaDrm = FrameworkMediaDrm.newInstance(uuid)
// TODO: This isn't very secure, should be fixed
if (hasDrmFailed) {
// When DRM fails using L1 we want to switch to L3
mediaDrm.setPropertyString("securityLevel", "L3")
}
return DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(uuid) { mediaDrm }
.setKeyRequestParameters(null)
.setMultiSession(drmProps.multiDrm)
.build(drmCallback)
} catch (ex: UnsupportedDrmException) {
hasDrmFailed = true
throw ex
} catch (ex: Exception) {
if (retryCount < 3) {
// Attempt retry 3 times in case where the OS Media DRM Framework fails for whatever reason
hasDrmFailed = true
return buildDrmSessionManager(uuid, drmProps, retryCount + 1)
}
throw UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, ex)
}
}
}

View File

@@ -0,0 +1,18 @@
package com.brentvatne.exoplayer
import androidx.media3.exoplayer.drm.DrmSessionManager
import androidx.media3.exoplayer.drm.UnsupportedDrmException
import com.brentvatne.common.api.DRMProps
import java.util.UUID
interface DRMManagerSpec {
/**
* Build a DRM session manager for the given UUID and DRM properties
* @param uuid The DRM system UUID
* @param drmProps The DRM properties from the source
* @return DrmSessionManager instance or null if not supported
* @throws UnsupportedDrmException if the DRM scheme is not supported
*/
@Throws(UnsupportedDrmException::class)
fun buildDrmSessionManager(uuid: UUID, drmProps: DRMProps): DrmSessionManager?
}

View File

@@ -5,13 +5,26 @@ import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
class DefaultReactExoplayerConfig(context: Context) : ReactExoplayerConfig {
class DefaultReactExoplayerConfig(private val context: Context, override var initialBitrate: Long? = null) : ReactExoplayerConfig {
private var bandWidthMeter: DefaultBandwidthMeter = createBandwidthMeter(initialBitrate)
private var bandWidthMeter: DefaultBandwidthMeter = DefaultBandwidthMeter.Builder(context).build()
override var disableDisconnectError: Boolean = false
override val bandwidthMeter: DefaultBandwidthMeter
get() = bandWidthMeter
private fun createBandwidthMeter(bitrate: Long?): DefaultBandwidthMeter =
DefaultBandwidthMeter.Builder(context)
.setInitialBitrateEstimate(bitrate ?: DefaultBandwidthMeter.DEFAULT_INITIAL_BITRATE_ESTIMATE)
.build()
override fun setInitialBitrate(bitrate: Long) {
if (initialBitrate == bitrate) return
initialBitrate = bitrate
bandWidthMeter = createBandwidthMeter(bitrate)
}
override fun buildLoadErrorHandlingPolicy(minLoadRetryCount: Int): LoadErrorHandlingPolicy =
if (disableDisconnectError) {
ReactExoplayerLoadErrorHandlingPolicy(minLoadRetryCount)

View File

@@ -1,279 +1,269 @@
package com.brentvatne.exoplayer
import android.content.Context
import android.util.Log
import android.util.TypedValue
import android.view.Gravity
import android.view.SurfaceView
import android.view.TextureView
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.view.View.MeasureSpec
import android.widget.FrameLayout
import androidx.core.content.ContextCompat
import androidx.media3.common.AdViewProvider
import androidx.media3.common.C
import android.widget.TextView
import androidx.media3.common.Player
import androidx.media3.common.Tracks
import androidx.media3.common.VideoSize
import androidx.media3.common.text.Cue
import androidx.media3.common.util.Assertions
import androidx.media3.common.Timeline
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.SubtitleView
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerView
import com.brentvatne.common.api.ResizeMode
import com.brentvatne.common.api.SubtitleStyle
import com.brentvatne.common.api.ViewType
import com.brentvatne.common.toolbox.DebugLog
@UnstableApi
class ExoPlayerView(private val context: Context) :
FrameLayout(context, null, 0),
AdViewProvider {
private var surfaceView: View? = null
private var shutterView: View
private var subtitleLayout: SubtitleView
private var layout: AspectRatioFrameLayout
private var componentListener: ComponentListener
private var player: ExoPlayer? = null
private var layoutParams: ViewGroup.LayoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
private var adOverlayFrameLayout: FrameLayout
val isPlaying: Boolean
get() = player != null && player?.isPlaying == true
@ViewType.ViewType
private var viewType = ViewType.VIEW_TYPE_SURFACE
private var hideShutterView = false
class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
FrameLayout(context, attrs, defStyleAttr) {
private var localStyle = SubtitleStyle()
private var pendingResizeMode: Int? = null
private val liveBadge: TextView = TextView(context).apply {
text = "LIVE"
setTextColor(Color.WHITE)
textSize = 12f
val drawable = GradientDrawable()
drawable.setColor(Color.RED)
drawable.cornerRadius = 6f
background = drawable
setPadding(12, 4, 12, 4)
visibility = View.GONE
}
private val playerView = PlayerView(context).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
setShutterBackgroundColor(Color.TRANSPARENT)
useController = true
controllerAutoShow = true
controllerHideOnTouch = true
controllerShowTimeoutMs = 5000
// Don't show subtitle button by default - will be enabled when tracks are available
setShowSubtitleButton(false)
// Enable proper surface view handling to prevent rendering issues
setUseArtwork(false)
setDefaultArtwork(null)
// Ensure proper video scaling - start with FIT mode
resizeMode = androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT
}
init {
componentListener = ComponentListener()
// Add PlayerView with explicit layout parameters
val playerViewLayoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
addView(playerView, playerViewLayoutParams)
val aspectRatioParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT
)
aspectRatioParams.gravity = Gravity.CENTER
layout = AspectRatioFrameLayout(context)
layout.layoutParams = aspectRatioParams
shutterView = View(context)
shutterView.layoutParams = layoutParams
shutterView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.black))
subtitleLayout = SubtitleView(context)
subtitleLayout.layoutParams = layoutParams
subtitleLayout.setUserDefaultStyle()
subtitleLayout.setUserDefaultTextSize()
updateSurfaceView(viewType)
adOverlayFrameLayout = FrameLayout(context)
layout.addView(shutterView, 1, layoutParams)
if (localStyle.subtitlesFollowVideo) {
layout.addView(subtitleLayout, layoutParams)
layout.addView(adOverlayFrameLayout, layoutParams)
// Add live badge with its own layout parameters
val liveBadgeLayoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
liveBadgeLayoutParams.setMargins(16, 16, 16, 16)
addView(liveBadge, liveBadgeLayoutParams)
}
addViewInLayout(layout, 0, aspectRatioParams)
if (!localStyle.subtitlesFollowVideo) {
addViewInLayout(subtitleLayout, 1, layoutParams)
}
fun setPlayer(player: ExoPlayer?) {
val currentPlayer = playerView.player
if (currentPlayer != null) {
currentPlayer.removeListener(playerListener)
}
private fun clearVideoView() {
when (val view = surfaceView) {
is TextureView -> player?.clearVideoTextureView(view)
playerView.player = player
is SurfaceView -> player?.clearVideoSurfaceView(view)
if (player != null) {
player.addListener(playerListener)
else -> {
Log.w(
"clearVideoView",
"Unexpected surfaceView type: ${surfaceView?.javaClass?.name}"
)
// Apply pending resize mode if we have one
pendingResizeMode?.let { resizeMode ->
playerView.resizeMode = resizeMode
}
}
}
private fun setVideoView() {
when (val view = surfaceView) {
is TextureView -> player?.setVideoTextureView(view)
fun getPlayerView(): PlayerView = playerView
is SurfaceView -> player?.setVideoSurfaceView(view)
fun setResizeMode(@ResizeMode.Mode resizeMode: Int) {
val targetResizeMode = when (resizeMode) {
ResizeMode.RESIZE_MODE_FILL -> AspectRatioFrameLayout.RESIZE_MODE_FILL
ResizeMode.RESIZE_MODE_CENTER_CROP -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
ResizeMode.RESIZE_MODE_FIT -> AspectRatioFrameLayout.RESIZE_MODE_FIT
ResizeMode.RESIZE_MODE_FIXED_WIDTH -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
ResizeMode.RESIZE_MODE_FIXED_HEIGHT -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
}
else -> {
Log.w(
"setVideoView",
"Unexpected surfaceView type: ${surfaceView?.javaClass?.name}"
)
}
}
// Apply the resize mode to PlayerView immediately
playerView.resizeMode = targetResizeMode
// Store it for reapplication if needed
pendingResizeMode = targetResizeMode
// Force PlayerView to recalculate its layout
playerView.requestLayout()
// Also request layout on the parent to ensure proper sizing
requestLayout()
}
fun setSubtitleStyle(style: SubtitleStyle) {
// ensure we reset subtitle style before reapplying it
subtitleLayout.setUserDefaultStyle()
subtitleLayout.setUserDefaultTextSize()
playerView.subtitleView?.let { subtitleView ->
// Reset to defaults
subtitleView.setUserDefaultStyle()
subtitleView.setUserDefaultTextSize()
// Apply custom styling
if (style.fontSize > 0) {
subtitleLayout.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, style.fontSize.toFloat())
subtitleView.setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, style.fontSize.toFloat())
}
subtitleLayout.setPadding(
subtitleView.setPadding(
style.paddingLeft,
style.paddingTop,
style.paddingTop,
style.paddingRight,
style.paddingBottom
)
if (style.opacity != 0.0f) {
subtitleLayout.alpha = style.opacity
subtitleLayout.visibility = View.VISIBLE
subtitleView.alpha = style.opacity
subtitleView.visibility = android.view.View.VISIBLE
} else {
subtitleLayout.visibility = View.GONE
subtitleView.visibility = android.view.View.GONE
}
if (localStyle.subtitlesFollowVideo != style.subtitlesFollowVideo) {
// No need to manipulate layout if value didn't change
if (style.subtitlesFollowVideo) {
removeViewInLayout(subtitleLayout)
layout.addView(subtitleLayout, layoutParams)
} else {
layout.removeViewInLayout(subtitleLayout)
addViewInLayout(subtitleLayout, 1, layoutParams, false)
}
requestLayout()
}
localStyle = style
}
fun setShutterColor(color: Int) {
shutterView.setBackgroundColor(color)
playerView.setShutterBackgroundColor(color)
}
fun updateSurfaceView(@ViewType.ViewType viewType: Int) {
this.viewType = viewType
var viewNeedRefresh = false
when (viewType) {
ViewType.VIEW_TYPE_SURFACE, ViewType.VIEW_TYPE_SURFACE_SECURE -> {
if (surfaceView !is SurfaceView) {
surfaceView = SurfaceView(context)
viewNeedRefresh = true
}
(surfaceView as SurfaceView).setSecure(viewType == ViewType.VIEW_TYPE_SURFACE_SECURE)
fun updateSurfaceView(viewType: Int) {
// TODO: Implement proper surface type switching if needed
}
ViewType.VIEW_TYPE_TEXTURE -> {
if (surfaceView !is TextureView) {
surfaceView = TextureView(context)
viewNeedRefresh = true
}
// Support opacity properly:
(surfaceView as TextureView).isOpaque = false
}
val isPlaying: Boolean
get() = playerView.player?.isPlaying ?: false
else -> {
DebugLog.wtf(TAG, "Unexpected texture view type: $viewType")
fun invalidateAspectRatio() {
// PlayerView handles aspect ratio automatically through its internal AspectRatioFrameLayout
playerView.requestLayout()
// Reapply the current resize mode to ensure it's properly set
pendingResizeMode?.let { resizeMode ->
playerView.resizeMode = resizeMode
}
}
if (viewNeedRefresh) {
surfaceView?.layoutParams = layoutParams
if (layout.getChildAt(0) != null) {
layout.removeViewAt(0)
fun setUseController(useController: Boolean) {
playerView.useController = useController
if (useController) {
// Ensure proper touch handling when controls are enabled
playerView.controllerAutoShow = true
playerView.controllerHideOnTouch = true
// Show controls immediately when enabled
playerView.showController()
}
}
layout.addView(surfaceView, 0, layoutParams)
if (this.player != null) {
setVideoView()
fun showController() {
playerView.showController()
}
fun hideController() {
playerView.hideController()
}
fun setControllerShowTimeoutMs(showTimeoutMs: Int) {
playerView.controllerShowTimeoutMs = showTimeoutMs
}
fun setControllerAutoShow(autoShow: Boolean) {
playerView.controllerAutoShow = autoShow
}
fun setControllerHideOnTouch(hideOnTouch: Boolean) {
playerView.controllerHideOnTouch = hideOnTouch
}
fun setFullscreenButtonClickListener(listener: PlayerView.FullscreenButtonClickListener?) {
playerView.setFullscreenButtonClickListener(listener)
}
fun setShowSubtitleButton(show: Boolean) {
playerView.setShowSubtitleButton(show)
}
fun isControllerVisible(): Boolean = playerView.isControllerFullyVisible
fun setControllerVisibilityListener(listener: PlayerView.ControllerVisibilityListener?) {
playerView.setControllerVisibilityListener(listener)
}
override fun addOnLayoutChangeListener(listener: View.OnLayoutChangeListener) {
playerView.addOnLayoutChangeListener(listener)
}
override fun setFocusable(focusable: Boolean) {
playerView.isFocusable = focusable
}
private fun updateLiveUi() {
val player = playerView.player ?: return
val isLive = player.isCurrentMediaItemLive
val seekable = player.isCurrentMediaItemSeekable
// Show/hide badge
liveBadge.visibility = if (isLive) View.VISIBLE else View.GONE
// Disable/enable scrubbing based on seekable
val timeBar = playerView.findViewById<DefaultTimeBar?>(androidx.media3.ui.R.id.exo_progress)
timeBar?.isEnabled = !isLive || seekable
}
private val playerListener = object : Player.Listener {
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
playerView.post {
playerView.requestLayout()
// Reapply resize mode to ensure it's properly set after timeline changes
pendingResizeMode?.let { resizeMode ->
playerView.resizeMode = resizeMode
}
}
updateLiveUi()
}
override fun onEvents(player: Player, events: Player.Events) {
if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION) ||
events.contains(Player.EVENT_IS_PLAYING_CHANGED)
) {
updateLiveUi()
}
// Handle video size changes which affect aspect ratio
if (events.contains(Player.EVENT_VIDEO_SIZE_CHANGED)) {
pendingResizeMode?.let { resizeMode ->
playerView.resizeMode = resizeMode
}
playerView.requestLayout()
requestLayout()
}
}
}
private fun hideShutterView() {
shutterView.setVisibility(INVISIBLE)
surfaceView?.setAlpha(1f)
}
private fun showShutterView() {
shutterView.setVisibility(VISIBLE)
surfaceView?.setAlpha(0f)
}
fun showAds() {
adOverlayFrameLayout.setVisibility(View.VISIBLE)
}
fun hideAds() {
adOverlayFrameLayout.setVisibility(View.GONE)
}
fun updateShutterViewVisibility() {
shutterView.visibility = if (this.hideShutterView) {
View.INVISIBLE
} else {
View.VISIBLE
}
}
override fun requestLayout() {
super.requestLayout()
post(measureAndLayout)
}
// AdsLoader.AdViewProvider implementation.
override fun getAdViewGroup(): ViewGroup =
Assertions.checkNotNull(
adOverlayFrameLayout,
"exo_ad_overlay must be present for ad playback"
)
/**
* Set the {@link ExoPlayer} to use. The {@link ExoPlayer#addListener} method of the
* player will be called and previous
* assignments are overridden.
*
* @param player The {@link ExoPlayer} to use.
*/
fun setPlayer(player: ExoPlayer?) {
if (this.player == player) {
return
}
if (this.player != null) {
this.player!!.removeListener(componentListener)
clearVideoView()
}
this.player = player
updateShutterViewVisibility()
if (player != null) {
setVideoView()
player.addListener(componentListener)
}
companion object {
private const val TAG = "ExoPlayerView"
}
/**
* Sets the resize mode which can be of value {@link ResizeMode.Mode}
*
* @param resizeMode The resize mode.
* React Native (Yoga) can sometimes defer layout passes that are required by
* PlayerView for its child views (controller overlay, surface view, subtitle view, …).
* This helper forces a second measure / layout after RN finishes, ensuring the
* internal views receive the final size. The same approach is used in the v7
* implementation (see VideoView.kt) and in React Native core (Toolbar example [link]).
*/
fun setResizeMode(@ResizeMode.Mode resizeMode: Int) {
if (layout.resizeMode != resizeMode) {
layout.resizeMode = resizeMode
post(measureAndLayout)
}
}
fun setHideShutterView(hideShutterView: Boolean) {
this.hideShutterView = hideShutterView
updateShutterViewVisibility()
}
private val measureAndLayout: Runnable = Runnable {
private val layoutRunnable = Runnable {
measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
@@ -281,58 +271,19 @@ class ExoPlayerView(private val context: Context) :
layout(left, top, right, bottom)
}
private fun updateForCurrentTrackSelections(tracks: Tracks?) {
if (tracks == null) {
return
}
val groups = tracks.groups
for (group in groups) {
if (group.type == C.TRACK_TYPE_VIDEO && group.length > 0) {
// get the first track of the group to identify aspect ratio
val format = group.getTrackFormat(0)
layout.updateAspectRatio(format)
return
}
}
// no video tracks, in that case refresh shutterView visibility
updateShutterViewVisibility()
override fun requestLayout() {
super.requestLayout()
// Post a second layout pass so the ExoPlayer internal views get correct bounds.
post(layoutRunnable)
}
fun invalidateAspectRatio() {
// Resetting aspect ratio will force layout refresh on next video size changed
layout.invalidateAspectRatio()
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
private inner class ComponentListener : Player.Listener {
override fun onCues(cues: List<Cue>) {
subtitleLayout.setCues(cues)
}
override fun onVideoSizeChanged(videoSize: VideoSize) {
if (videoSize.height == 0 || videoSize.width == 0) {
// When changing video track we receive an ghost state with height / width = 0
// No need to resize the view in that case
return
}
// Here we use updateForCurrentTrackSelections to have a consistent behavior.
// according to: https://github.com/androidx/media/issues/1207
// sometimes media3 send bad Video size information
player?.let {
updateForCurrentTrackSelections(it.currentTracks)
if (changed) {
pendingResizeMode?.let { resizeMode ->
playerView.resizeMode = resizeMode
}
}
override fun onRenderedFirstFrame() {
shutterView.visibility = INVISIBLE
}
override fun onTracksChanged(tracks: Tracks) {
updateForCurrentTrackSelections(tracks)
}
}
companion object {
private const val TAG = "ExoPlayerView"
}
}

View File

@@ -5,12 +5,11 @@ import android.app.Dialog
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.activity.OnBackPressedCallback
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
@@ -90,7 +89,6 @@ class FullScreenPlayerView(
parent?.removeView(exoPlayerView)
containerView.addView(exoPlayerView, generateDefaultLayoutParams())
playerControlView?.let {
updateFullscreenButton(playerControlView, true)
parent?.removeView(it)
containerView.addView(it, generateDefaultLayoutParams())
}
@@ -103,7 +101,6 @@ class FullScreenPlayerView(
containerView.removeView(exoPlayerView)
parent?.addView(exoPlayerView, generateDefaultLayoutParams())
playerControlView?.let {
updateFullscreenButton(playerControlView, false)
containerView.removeView(it)
parent?.addView(it, generateDefaultLayoutParams())
}
@@ -125,6 +122,14 @@ class FullScreenPlayerView(
}
}
fun hideWithoutPlayer() {
for (i in 0 until containerView.childCount) {
if (containerView.getChildAt(i) !== exoPlayerView) {
containerView.getChildAt(i).visibility = View.GONE
}
}
}
private fun getFullscreenIconResource(isFullscreen: Boolean): Int =
if (isFullscreen) {
androidx.media3.ui.R.drawable.exo_icon_fullscreen_exit
@@ -132,20 +137,6 @@ class FullScreenPlayerView(
androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter
}
private fun updateFullscreenButton(playerControlView: LegacyPlayerControlView, isFullscreen: Boolean) {
val imageButton = playerControlView.findViewById<ImageButton?>(com.brentvatne.react.R.id.exo_fullscreen)
imageButton?.let {
val imgResource = getFullscreenIconResource(isFullscreen)
val desc = if (isFullscreen) {
context.getString(androidx.media3.ui.R.string.exo_controls_fullscreen_exit_description)
} else {
context.getString(androidx.media3.ui.R.string.exo_controls_fullscreen_enter_description)
}
imageButton.setImageResource(imgResource)
imageButton.contentDescription = desc
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (reactExoplayerView.preventsDisplaySleepDuringVideoPlayback) {
@@ -217,13 +208,7 @@ class FullScreenPlayerView(
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
)
}
if (controlsConfig.hideNotificationBarOnFullScreenMode) {
val liveContainer = playerControlView?.findViewById<LinearLayout?>(com.brentvatne.react.R.id.exo_live_container)
liveContainer?.let {
val layoutParams = it.layoutParams as LinearLayout.LayoutParams
layoutParams.topMargin = 40
it.layoutParams = layoutParams
}
}
// Note: Live container adjustment is no longer needed since we're using PlayerView's built-in controls
// PlayerView handles UI adjustments automatically
}
}

View File

@@ -0,0 +1,213 @@
package com.brentvatne.exoplayer
import android.annotation.SuppressLint
import android.app.AppOpsManager
import android.app.PictureInPictureParams
import android.app.RemoteAction
import android.content.Context
import android.content.ContextWrapper
import android.content.pm.PackageManager
import android.graphics.Rect
import android.graphics.drawable.Icon
import android.os.Build
import android.os.Process
import android.util.Rational
import androidx.activity.ComponentActivity
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.annotation.RequiresApi
import androidx.core.app.AppOpsManagerCompat
import androidx.core.app.PictureInPictureModeChangedInfo
import androidx.core.util.Consumer
import androidx.lifecycle.Lifecycle
import androidx.media3.exoplayer.ExoPlayer
import com.brentvatne.common.toolbox.DebugLog
import com.brentvatne.receiver.PictureInPictureReceiver
import com.facebook.react.uimanager.ThemedReactContext
internal fun Context.findActivity(): ComponentActivity {
var context = this
while (context is ContextWrapper) {
if (context is ComponentActivity) return context
context = context.baseContext
}
throw IllegalStateException("Picture in picture should be called in the context of an Activity")
}
object PictureInPictureUtil {
private const val FLAG_SUPPORTS_PICTURE_IN_PICTURE = 0x400000
private const val TAG = "PictureInPictureUtil"
@JvmStatic
fun addLifecycleEventListener(context: ThemedReactContext, view: ReactExoplayerView): Runnable {
val activity = context.findActivity()
val onPictureInPictureModeChanged = Consumer<PictureInPictureModeChangedInfo> { info ->
view.setIsInPictureInPicture(info.isInPictureInPictureMode)
if (!info.isInPictureInPictureMode && activity.lifecycle.currentState == Lifecycle.State.CREATED) {
// when user click close button of PIP
if (!view.playInBackground) view.setPausedModifier(true)
}
}
val onUserLeaveHintCallback = Runnable {
if (view.enterPictureInPictureOnLeave) {
view.enterPictureInPictureMode()
}
}
activity.addOnPictureInPictureModeChangedListener(onPictureInPictureModeChanged)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
activity.addOnUserLeaveHintListener(onUserLeaveHintCallback)
}
// @TODO convert to lambda when ReactExoplayerView migrated
return Runnable {
with(activity) {
removeOnPictureInPictureModeChangedListener(onPictureInPictureModeChanged)
removeOnUserLeaveHintListener(onUserLeaveHintCallback)
}
}
}
@JvmStatic
fun enterPictureInPictureMode(context: ThemedReactContext, pictureInPictureParams: PictureInPictureParams?) {
if (!isSupportPictureInPicture(context)) return
if (isSupportPictureInPictureAction() && pictureInPictureParams != null) {
try {
context.findActivity().enterPictureInPictureMode(pictureInPictureParams)
} catch (e: IllegalStateException) {
DebugLog.e(TAG, e.toString())
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
@Suppress("DEPRECATION")
context.findActivity().enterPictureInPictureMode()
} catch (e: IllegalStateException) {
DebugLog.e(TAG, e.toString())
}
}
}
@JvmStatic
fun applyPlayingStatus(
context: ThemedReactContext,
pipParamsBuilder: PictureInPictureParams.Builder?,
receiver: PictureInPictureReceiver,
isPaused: Boolean
) {
if (pipParamsBuilder == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val actions = getPictureInPictureActions(context, isPaused, receiver)
pipParamsBuilder.setActions(actions)
updatePictureInPictureActions(context, pipParamsBuilder.build())
}
@JvmStatic
fun applyAutoEnterEnabled(context: ThemedReactContext, pipParamsBuilder: PictureInPictureParams.Builder?, autoEnterEnabled: Boolean) {
if (pipParamsBuilder == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
pipParamsBuilder.setAutoEnterEnabled(autoEnterEnabled)
updatePictureInPictureActions(context, pipParamsBuilder.build())
}
@JvmStatic
fun applySourceRectHint(context: ThemedReactContext, pipParamsBuilder: PictureInPictureParams.Builder?, playerView: ExoPlayerView) {
if (pipParamsBuilder == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
pipParamsBuilder.setSourceRectHint(calcRectHint(playerView))
updatePictureInPictureActions(context, pipParamsBuilder.build())
}
private fun updatePictureInPictureActions(context: ThemedReactContext, pipParams: PictureInPictureParams) {
if (!isSupportPictureInPictureAction()) return
if (!isSupportPictureInPicture(context)) return
try {
context.findActivity().setPictureInPictureParams(pipParams)
} catch (e: IllegalStateException) {
DebugLog.e(TAG, e.toString())
}
}
@JvmStatic
@RequiresApi(Build.VERSION_CODES.O)
fun getPictureInPictureActions(context: ThemedReactContext, isPaused: Boolean, receiver: PictureInPictureReceiver): ArrayList<RemoteAction> {
val intent = receiver.getPipActionIntent(isPaused)
val resource =
if (isPaused) androidx.media3.ui.R.drawable.exo_icon_play else androidx.media3.ui.R.drawable.exo_icon_pause
val icon = Icon.createWithResource(context, resource)
val title = if (isPaused) "play" else "pause"
return arrayListOf(RemoteAction(icon, title, title, intent))
}
@JvmStatic
@RequiresApi(Build.VERSION_CODES.O)
private fun calcRectHint(playerView: ExoPlayerView): Rect {
val hint = Rect()
// Use the PlayerView itself since surfaceView is private
playerView.getGlobalVisibleRect(hint)
val location = IntArray(2)
playerView.getLocationOnScreen(location)
val height = hint.bottom - hint.top
hint.top = location[1]
hint.bottom = hint.top + height
return hint
}
@JvmStatic
@RequiresApi(Build.VERSION_CODES.O)
fun calcPictureInPictureAspectRatio(player: ExoPlayer): Rational {
var aspectRatio = Rational(player.videoSize.width, player.videoSize.height)
// AspectRatio for the activity in picture-in-picture, must be between 2.39:1 and 1:2.39 (inclusive).
// https://developer.android.com/reference/android/app/PictureInPictureParams.Builder#setAspectRatio(android.util.Rational)
val maximumRatio = Rational(239, 100)
val minimumRatio = Rational(100, 239)
if (aspectRatio.toFloat() > maximumRatio.toFloat()) {
aspectRatio = maximumRatio
} else if (aspectRatio.toFloat() < minimumRatio.toFloat()) {
aspectRatio = minimumRatio
}
return aspectRatio
}
private fun isSupportPictureInPicture(context: ThemedReactContext): Boolean =
checkIsApiSupport() && checkIsSystemSupportPIP(context) && checkIsUserAllowPIP(context)
private fun isSupportPictureInPictureAction(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.N)
private fun checkIsApiSupport(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
@RequiresApi(Build.VERSION_CODES.N)
private fun checkIsSystemSupportPIP(context: ThemedReactContext): Boolean {
val activity = context.findActivity() ?: return false
val isActivitySupportPip = try {
val activityInfo = activity.packageManager.getActivityInfo(activity.componentName, PackageManager.GET_META_DATA)
// detect current activity's android:supportsPictureInPicture value defined within AndroidManifest.xml
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/pm/ActivityInfo.java;l=1090-1093;drc=7651f0a4c059a98f32b0ba30cd64500bf135385f
activityInfo.flags and FLAG_SUPPORTS_PICTURE_IN_PICTURE != 0
} catch (e: kotlin.Exception) {
false
}
// PIP might be disabled on devices that have low RAM.
val isPipAvailable = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
return isActivitySupportPip && isPipAvailable
}
private fun checkIsUserAllowPIP(context: ThemedReactContext): Boolean {
val activity = context.currentActivity ?: return false
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@SuppressLint("InlinedApi")
val result = AppOpsManagerCompat.noteOpNoThrow(
activity,
AppOpsManager.OPSTR_PICTURE_IN_PICTURE,
Process.myUid(),
activity.packageName
)
AppOpsManager.MODE_ALLOWED == result
} else {
Build.VERSION.SDK_INT < Build.VERSION_CODES.O && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
}
}
}

View File

@@ -0,0 +1,98 @@
package com.brentvatne.exoplayer
import androidx.media3.common.MediaItem
import androidx.media3.datasource.DataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.drm.DrmSessionManager
import androidx.media3.exoplayer.source.MediaSource
import com.brentvatne.common.api.Source
import com.brentvatne.react.RNVPlugin
/**
* Interface for RNV plugins that have dependencies or logic that is specific to Exoplayer
* It extends the RNVPlugin interface
*/
interface RNVExoplayerPlugin : RNVPlugin {
/**
* Optional function that allows plugin to provide custom DRM manager
* Only one plugin can provide DRM manager at a time
* @return DRMManagerSpec implementation if plugin wants to handle DRM, null otherwise
*/
fun getDRMManager(): DRMManagerSpec? = null
/**
* Optional function that allows the plugin to override the DrmSessionManager after it has been created.
* This is called after buildDrmSessionManager and allows for final modifications to the DrmSessionManager.
* @param source The media source being initialized.
* @param drmSessionManager The current DrmSessionManager instance.
* @return A modified DrmSessionManager if override is needed, or null to use original.
*/
fun overrideDrmSessionManager(source: Source, drmSessionManager: DrmSessionManager): DrmSessionManager? = null
/**
* Optional function that allows the plugin to override the media data source factory,
* which is responsible for loading media data.
* @param source The media source being initialized.
* @param mediaDataSourceFactory The current default data source factory.
* @return A custom [DataSource.Factory] if override is needed, or null to use default.
*/
fun overrideMediaDataSourceFactory(source: Source, mediaDataSourceFactory: DataSource.Factory): DataSource.Factory? = null
/**
* Optional function that allows the plugin to override the media source factory,
* which is responsible for loading media data.
* @param source The media source being initialized.
* @param mediaSourceFactory The current media source factory.
* @param mediaDataSourceFactory The current default data source factory.
* @return A custom [MediaSource.Factory] if override is needed, or null to use default.
*/
fun overrideMediaSourceFactory(source: Source, mediaSourceFactory: MediaSource.Factory, mediaDataSourceFactory: DataSource.Factory): MediaSource.Factory? =
null
/**
* Optional function that allows the plugin to modify the [MediaItem.Builder]
* before the final [MediaItem] is created.
* @param source The source from which the media item is being built.
* @param mediaItemBuilder The default [MediaItem.Builder] instance.
* @return A modified builder instance if override is needed, or null to use original.
*/
fun overrideMediaItemBuilder(source: Source, mediaItemBuilder: MediaItem.Builder): MediaItem.Builder? = null
/**
* Optional function that allows the plugin to control whether caching should be disabled
* for a given video source.
* @param source The video source being loaded.
* @return true to disable caching, false to keep it enabled.
*/
fun shouldDisableCache(source: Source): Boolean = false
/**
* Function called when a new player is created
* @param id: a random string identifying the player
* @param player: the instantiated player reference
* @note: This is helper that ensure that player is non null ExoPlayer
*/
fun onInstanceCreated(id: String, player: ExoPlayer)
/**
* Function called when a player should be destroyed
* when this callback is called, the plugin shall free all
* resources and release all reference to Player object
* @param id: a random string identifying the player
* @param player: the player to release
* @note: This is helper that ensure that player is non null ExoPlayer
*/
fun onInstanceRemoved(id: String, player: ExoPlayer)
override fun onInstanceCreated(id: String, player: Any) {
if (player is ExoPlayer) {
onInstanceCreated(id, player)
}
}
override fun onInstanceRemoved(id: String, player: Any) {
if (player is ExoPlayer) {
onInstanceRemoved(id, player)
}
}
}

View File

@@ -7,4 +7,6 @@ interface ReactExoplayerConfig {
fun buildLoadErrorHandlingPolicy(minLoadRetryCount: Int): LoadErrorHandlingPolicy
var disableDisconnectError: Boolean
val bandwidthMeter: DefaultBandwidthMeter
var initialBitrate: Long?
fun setInitialBitrate(bitrate: Long)
}

View File

@@ -2,7 +2,6 @@ package com.brentvatne.exoplayer
import android.graphics.Color
import android.util.Log
import com.brentvatne.common.api.BufferConfig
import com.brentvatne.common.api.BufferingStrategy
import com.brentvatne.common.api.ControlsConfig
import com.brentvatne.common.api.ResizeMode
@@ -33,16 +32,15 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
private const val PROP_SELECTED_TEXT_TRACK_TYPE = "type"
private const val PROP_SELECTED_TEXT_TRACK_VALUE = "value"
private const val PROP_PAUSED = "paused"
private const val PROP_ENTER_PICTURE_IN_PICTURE_ON_LEAVE = "enterPictureInPictureOnLeave"
private const val PROP_MUTED = "muted"
private const val PROP_AUDIO_OUTPUT = "audioOutput"
private const val PROP_VOLUME = "volume"
private const val PROP_BUFFER_CONFIG = "bufferConfig"
private const val PROP_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK =
"preventsDisplaySleepDuringVideoPlayback"
private const val PROP_PROGRESS_UPDATE_INTERVAL = "progressUpdateInterval"
private const val PROP_REPORT_BANDWIDTH = "reportBandwidth"
private const val PROP_RATE = "rate"
private const val PROP_MIN_LOAD_RETRY_COUNT = "minLoadRetryCount"
private const val PROP_MAXIMUM_BIT_RATE = "maxBitRate"
private const val PROP_PLAY_IN_BACKGROUND = "playInBackground"
private const val PROP_DISABLE_FOCUS = "disableFocus"
@@ -54,7 +52,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
private const val PROP_SELECTED_VIDEO_TRACK = "selectedVideoTrack"
private const val PROP_SELECTED_VIDEO_TRACK_TYPE = "type"
private const val PROP_SELECTED_VIDEO_TRACK_VALUE = "value"
private const val PROP_HIDE_SHUTTER_VIEW = "hideShutterView"
private const val PROP_CONTROLS = "controls"
private const val PROP_SUBTITLE_STYLE = "subtitleStyle"
private const val PROP_SHUTTER_COLOR = "shutterColor"
@@ -72,6 +69,7 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
override fun onDropViewInstance(view: ReactExoplayerView) {
view.cleanUpResources()
view.exitPictureInPictureMode()
ReactNativeVideoManager.getInstance().unregisterView(this)
}
@@ -157,6 +155,11 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
videoView.setMutedModifier(muted)
}
@ReactProp(name = PROP_ENTER_PICTURE_IN_PICTURE_ON_LEAVE, defaultBoolean = false)
fun setEnterPictureInPictureOnLeave(videoView: ReactExoplayerView, enterPictureInPictureOnLeave: Boolean) {
videoView.setEnterPictureInPictureOnLeave(enterPictureInPictureOnLeave)
}
@ReactProp(name = PROP_AUDIO_OUTPUT)
fun setAudioOutput(videoView: ReactExoplayerView, audioOutput: String) {
videoView.setAudioOutput(AudioOutput.get(audioOutput))
@@ -187,11 +190,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
videoView.setMaxBitRateModifier(maxBitRate.toInt())
}
@ReactProp(name = PROP_MIN_LOAD_RETRY_COUNT)
fun setMinLoadRetryCount(videoView: ReactExoplayerView, minLoadRetryCount: Int) {
videoView.setMinLoadRetryCountModifier(minLoadRetryCount)
}
@ReactProp(name = PROP_PLAY_IN_BACKGROUND, defaultBoolean = false)
fun setPlayInBackground(videoView: ReactExoplayerView, playInBackground: Boolean) {
videoView.setPlayInBackground(playInBackground)
@@ -228,11 +226,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
videoView.setViewType(viewType)
}
@ReactProp(name = PROP_HIDE_SHUTTER_VIEW, defaultBoolean = false)
fun setHideShutterView(videoView: ReactExoplayerView, hideShutterView: Boolean) {
videoView.setHideShutterView(hideShutterView)
}
@ReactProp(name = PROP_CONTROLS, defaultBoolean = false)
fun setControls(videoView: ReactExoplayerView, controls: Boolean) {
videoView.setControls(controls)
@@ -248,12 +241,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
videoView.setShutterColor(color)
}
@ReactProp(name = PROP_BUFFER_CONFIG)
fun setBufferConfig(videoView: ReactExoplayerView, bufferConfig: ReadableMap?) {
val config = BufferConfig.parse(bufferConfig)
videoView.setBufferConfig(config)
}
@ReactProp(name = PROP_SHOW_NOTIFICATION_CONTROLS)
fun setShowNotificationControls(videoView: ReactExoplayerView, showNotificationControls: Boolean) {
videoView.setShowNotificationControls(showNotificationControls)

View File

@@ -20,6 +20,7 @@ import androidx.media3.session.MediaSessionService
import androidx.media3.session.MediaStyleNotificationHelper
import androidx.media3.session.SessionCommand
import com.brentvatne.common.toolbox.DebugLog
import com.brentvatne.react.R
import okhttp3.internal.immutableListOf
class PlaybackServiceBinder(val service: VideoPlaybackService) : Binder()
@@ -63,7 +64,9 @@ class VideoPlaybackService : MediaSessionService() {
mediaSessionsList[player] = mediaSession
addSession(mediaSession)
startForeground(mediaSession.player.hashCode(), buildNotification(mediaSession))
val notificationId = player.hashCode()
startForeground(notificationId, buildNotification(mediaSession))
}
fun unregisterPlayer(player: ExoPlayer) {
@@ -224,7 +227,30 @@ class VideoPlaybackService : MediaSessionService() {
mediaSessionsList.clear()
}
private fun createPlaceholderNotification(): Notification {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.createNotificationChannel(
NotificationChannel(
NOTIFICATION_CHANEL_ID,
NOTIFICATION_CHANEL_ID,
NotificationManager.IMPORTANCE_LOW
)
)
}
return NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID)
.setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
.setContentTitle(getString(R.string.media_playback_notification_title))
.setContentText(getString(R.string.media_playback_notification_text))
.build()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForeground(PLACEHOLDER_NOTIFICATION_ID, createPlaceholderNotification())
}
intent?.let {
val playerId = it.getIntExtra("PLAYER_ID", -1)
val actionCommand = it.getStringExtra("ACTION")
@@ -249,6 +275,7 @@ class VideoPlaybackService : MediaSessionService() {
companion object {
private const val SEEK_INTERVAL_MS = 10000L
private const val TAG = "VideoPlaybackService"
private const val PLACEHOLDER_NOTIFICATION_ID = 9999
const val NOTIFICATION_CHANEL_ID = "RNVIDEO_SESSION_NOTIFICATION"

View File

@@ -1,7 +1,8 @@
package com.brentvatne.react
/**
* Plugin interface definition
* Plugin interface definition for RNV plugins that does not have dependencies nor logic specific to any player
* It is the base interface for all RNV plugins
*/
interface RNVPlugin {
/**

View File

@@ -1,11 +1,17 @@
package com.brentvatne.react
import androidx.media3.common.MediaItem
import androidx.media3.datasource.DataSource
import androidx.media3.exoplayer.drm.DrmSessionManager
import androidx.media3.exoplayer.source.MediaSource
import com.brentvatne.common.api.Source
import com.brentvatne.common.toolbox.DebugLog
import com.brentvatne.exoplayer.ReactExoplayerViewManager
import com.brentvatne.exoplayer.DRMManagerSpec
import com.brentvatne.exoplayer.RNVExoplayerPlugin
/**
* ReactNativeVideoManager is a singleton class which allows to manipulate / the global state of the app
* It handles the list of <Video view instanced and registration of plugins
* It handles the list of <Video/> view instanced and registration of plugins
*/
class ReactNativeVideoManager : RNVPlugin {
companion object {
@@ -23,13 +29,14 @@ class ReactNativeVideoManager : RNVPlugin {
}
}
private var instanceList: ArrayList<ReactExoplayerViewManager> = ArrayList()
private var pluginList: ArrayList<RNVPlugin> = ArrayList()
private val pluginList = ArrayList<RNVPlugin>()
private var customDRMManager: DRMManagerSpec? = null
private var instanceList: ArrayList<Any> = ArrayList()
/**
* register a new ReactExoplayerViewManager in the managed list
*/
fun registerView(newInstance: ReactExoplayerViewManager) {
fun registerView(newInstance: Any) {
if (instanceList.size > 2) {
DebugLog.d(TAG, "multiple Video displayed ?")
}
@@ -39,7 +46,7 @@ class ReactNativeVideoManager : RNVPlugin {
/**
* unregister existing ReactExoplayerViewManager in the managed list
*/
fun unregisterView(newInstance: ReactExoplayerViewManager) {
fun unregisterView(newInstance: Any) {
instanceList.remove(newInstance)
}
@@ -48,7 +55,8 @@ class ReactNativeVideoManager : RNVPlugin {
*/
fun registerPlugin(plugin: RNVPlugin) {
pluginList.add(plugin)
return
maybeRegisterExoplayerPlugin(plugin)
}
/**
@@ -56,9 +64,11 @@ class ReactNativeVideoManager : RNVPlugin {
*/
fun unregisterPlugin(plugin: RNVPlugin) {
pluginList.remove(plugin)
return
maybeUnregisterExoplayerPlugin(plugin)
}
// ----------------------- Generic RNV plugin methods -----------------------
override fun onInstanceCreated(id: String, player: Any) {
pluginList.forEach { it.onInstanceCreated(id, player) }
}
@@ -66,4 +76,83 @@ class ReactNativeVideoManager : RNVPlugin {
override fun onInstanceRemoved(id: String, player: Any) {
pluginList.forEach { it.onInstanceRemoved(id, player) }
}
// ----------------------- RNV Exoplayer plugin specific methods -----------------------
fun getDRMManager(): DRMManagerSpec? = customDRMManager
fun overrideDrmSessionManager(source: Source, drmSessionManager: DrmSessionManager): DrmSessionManager? {
for (plugin in pluginList) {
if (plugin !is RNVExoplayerPlugin) continue
val overriddenManager = plugin.overrideDrmSessionManager(source, drmSessionManager)
if (overriddenManager != null) return overriddenManager
}
return null
}
fun overrideMediaDataSourceFactory(source: Source, mediaDataSourceFactory: DataSource.Factory): DataSource.Factory? {
for (plugin in pluginList) {
if (plugin !is RNVExoplayerPlugin) continue
val factory = plugin.overrideMediaDataSourceFactory(source, mediaDataSourceFactory)
if (factory != null) return factory
}
return null
}
fun overrideMediaSourceFactory(source: Source, mediaSourceFactory: MediaSource.Factory, mediaDataSourceFactory: DataSource.Factory): MediaSource.Factory? {
for (plugin in pluginList) {
if (plugin !is RNVExoplayerPlugin) continue
val factory = plugin.overrideMediaSourceFactory(source, mediaSourceFactory, mediaDataSourceFactory)
if (factory != null) return factory
}
return null
}
fun overrideMediaItemBuilder(source: Source, mediaItemBuilder: MediaItem.Builder): MediaItem.Builder? {
for (plugin in pluginList) {
if (plugin !is RNVExoplayerPlugin) continue
val builder = plugin.overrideMediaItemBuilder(source, mediaItemBuilder)
if (builder != null) return builder
}
return null
}
fun shouldDisableCache(source: Source): Boolean {
for (plugin in pluginList) {
if (plugin is RNVExoplayerPlugin && plugin.shouldDisableCache(source)) {
return true
}
}
return false
}
// ----------------------- Custom Plugins Helpers -----------------------
private fun maybeRegisterExoplayerPlugin(plugin: RNVPlugin) {
if (plugin !is RNVExoplayerPlugin) {
return
}
// Check if plugin provides DRM manager
plugin.getDRMManager()?.let { drmManager ->
if (customDRMManager != null) {
DebugLog.w("ReactNativeVideoManager", "Multiple DRM managers registered. This is not supported. Using first registered manager.")
return@let
}
customDRMManager = drmManager
}
}
private fun maybeUnregisterExoplayerPlugin(plugin: RNVPlugin) {
if (plugin !is RNVExoplayerPlugin) {
return
}
// If this plugin provided the DRM manager, remove it
if (plugin.getDRMManager() === customDRMManager) {
customDRMManager = null
}
}
}

View File

@@ -65,6 +65,20 @@ class VideoManagerModule(reactContext: ReactApplicationContext?) : ReactContextB
}
}
@ReactMethod
fun enterPictureInPictureCmd(reactTag: Int) {
performOnPlayerView(reactTag) {
it?.enterPictureInPictureMode()
}
}
@ReactMethod
fun exitPictureInPictureCmd(reactTag: Int) {
performOnPlayerView(reactTag) {
it?.exitPictureInPictureMode()
}
}
@ReactMethod
fun setSourceCmd(reactTag: Int, source: ReadableMap?) {
performOnPlayerView(reactTag) {

View File

@@ -0,0 +1,72 @@
package com.brentvatne.receiver
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import androidx.core.content.ContextCompat
import com.brentvatne.exoplayer.ReactExoplayerView
import com.facebook.react.uimanager.ThemedReactContext
class PictureInPictureReceiver(private val view: ReactExoplayerView, private val context: ThemedReactContext) : BroadcastReceiver() {
companion object {
const val ACTION_MEDIA_CONTROL = "rnv_media_control"
const val EXTRA_CONTROL_TYPE = "rnv_control_type"
// The request code for play action PendingIntent.
const val REQUEST_PLAY = 1
// The request code for pause action PendingIntent.
const val REQUEST_PAUSE = 2
// The intent extra value for play action.
const val CONTROL_TYPE_PLAY = 1
// The intent extra value for pause action.
const val CONTROL_TYPE_PAUSE = 2
}
override fun onReceive(context: Context?, intent: Intent?) {
intent ?: return
if (intent.action == ACTION_MEDIA_CONTROL) {
when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) {
CONTROL_TYPE_PLAY -> view.setPausedModifier(false)
CONTROL_TYPE_PAUSE -> view.setPausedModifier(true)
}
}
}
fun setListener() {
ContextCompat.registerReceiver(context, this, IntentFilter(ACTION_MEDIA_CONTROL), ContextCompat.RECEIVER_NOT_EXPORTED)
}
fun removeListener() {
try {
context.unregisterReceiver(this)
} catch (e: Exception) {
// ignore if already unregistered
}
}
fun getPipActionIntent(isPaused: Boolean): PendingIntent {
val requestCode = if (isPaused) REQUEST_PLAY else REQUEST_PAUSE
val controlType = if (isPaused) CONTROL_TYPE_PLAY else CONTROL_TYPE_PAUSE
val flag =
if (Build.VERSION.SDK_INT >=
Build.VERSION_CODES.M
) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
val intent = Intent(ACTION_MEDIA_CONTROL).putExtra(
EXTRA_CONTROL_TYPE,
controlType
)
intent.setPackage(context.packageName)
return PendingIntent.getBroadcast(context, requestCode, intent, flag)
}
}

View File

@@ -3,7 +3,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutDirection="ltr"
android:background="@color/midnight_black"
android:background="@color/player_overlay_color"
android:orientation="vertical">
<LinearLayout

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="silver_gray">#FFBEBEBE</color>
<color name="midnight_black">#CC000000</color>
<color name="player_overlay_color">#00000000</color>
<color name="white">#FFFFFF</color>
<color name="red">#FF0000</color>
</resources>

View File

@@ -22,4 +22,8 @@
<string name="playback_speed">Playback Speed</string>
<string name="select_playback_speed">Select Playback Speed</string>
<string name="media_playback_notification_title">Media playback</string>
<string name="media_playback_notification_text">Preparing playback</string>
</resources>

2
docs/.gitignore vendored
View File

@@ -1,3 +1,5 @@
node_modules/
out/
.next/
public/llms.txt
public/llms-full.txt

View File

@@ -1,15 +1,3 @@
# react-native-video-docs
This is the documentation for the [react-native-video](github.com/TheWidlarzGroup/react-native-video).
Project is using [bun](https://bun.sh) to build and run the documentation.
Framework for static site generation is [Nextra](https://nextra.site/docs)
```bash
bun install
```
To run:
```bash
bun run dev
```
Documentation have been moved to [`v7`](https://github.com/TheWidlarzGroup/react-native-video/tree/v7/docs) branch. Any changes should be made there. Any changes to `v7` branch will be automatically deployed to [docs.thewidlarzgroup.com](https://docs.thewidlarzgroup.com/react-native-video). Changed made in `master` branch will be be ignored.

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

View File

@@ -3,6 +3,6 @@
}
.spanStyle {
font-family: 'Orbitron';
font-family: var(--font-orbitron);
font-weight: 800;
}

View File

@@ -0,0 +1,62 @@
.extraContainer {
display: flex;
flex-direction: column;
margin-top: 0.5rem;
text-align: center;
background-color: #171717;
padding: 1rem;
gap: 1rem;
border-radius: 0.5rem;
}
.extraText {
padding-left: 0.5rem;
padding-right: 0.5rem;
font-weight: bold;
color: #fff;
}
.extraButton {
width: 100%;
border: none;
padding: 0.5rem 1rem;
font-weight: 500;
background-color: #f9d85b;
transition: transform 0.3s ease, background-color 0.3s ease;
}
.extraButton:hover {
transform: scale(1.05);
background-color: #fff;
}
:is(html[class~=dark]) .extraContainer {
background-color: #87ccef;
}
:is(html[class~=dark]) .extraText {
color: #171717;
}
:is(html[class~=dark]) .extraButton {
background-color: #171717;
}
@media (min-width: 1280px) {
.visibleOnLarge {
display: inherit;
}
.visibleOnSmall {
display: none;
}
}
@media (max-width: 1279px) {
.visibleOnLarge {
display: none;
}
.visibleOnSmall {
display: flex;
}
}

View File

@@ -0,0 +1,27 @@
import React from 'react';
import styles from './TWGBadge.module.css';
interface TWGBadgeProps {
visibleOnLarge?: boolean;
}
const TWGBadge = ({visibleOnLarge}: TWGBadgeProps) => {
const visibilityClass = visibleOnLarge
? styles.visibleOnLarge
: styles.visibleOnSmall;
return (
<div className={[styles.extraContainer, visibilityClass].join(' ')}>
<span className={styles.extraText}>We are TheWidlarzGroup</span>
<a
target="_blank"
href="https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs&utm_campaign=badge&utm_id=enterprise#Contact"
className={styles.extraButton}
rel="noreferrer">
Premium support
</a>
</div>
);
};
export default TWGBadge;

7
docs/font.ts Normal file
View File

@@ -0,0 +1,7 @@
import {Orbitron} from 'next/font/google';
export const orbitron = Orbitron({
display: 'swap',
subsets: ['latin'],
weight: ['400', '900'],
});

56
docs/json-ld.json Normal file
View File

@@ -0,0 +1,56 @@
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "TheWidlarzGroup Docs",
"url": "https://docs.thewidlarzgroup.com/react-native-video/",
"publisher": {
"@type": "Organization",
"name": "TheWidlarzGroup",
"url": "https://thewidlarzgroup.com",
"logo": {
"@type": "ImageObject",
"url": "https://docs.thewidlarzgroup.com/react-native-video/favicon.ico"
}
},
"mainEntity": {
"@type": "TechArticle",
"headline": "React Native Video Documentation",
"description": "Official documentation for React Native Video component by TheWidlarzGroup",
"hasPart": [
{
"@type": "TechArticle",
"headline": "Installation Guide",
"description": "Installation instructions for your platform to link react-native-video into your project.",
"url": "https://docs.thewidlarzgroup.com/react-native-video/installation"
},
{
"@type": "TechArticle",
"headline": "Properties",
"description": "This page shows the list of available properties to configure the player.",
"url": "https://docs.thewidlarzgroup.com/react-native-video/component/props"
},
{
"@type": "TechArticle",
"headline": "DRM Implementation",
"description": "We provide a sample implementation in the example app demonstrating how to use DRM with react-native-video.",
"url": "https://docs.thewidlarzgroup.com/react-native-video/component/drm"
},
{
"@type": "TechArticle",
"headline": "Downloading",
"description": "The Offline Video SDK extends react-native-video (v6 or v7) with the ability to download and store video content for offline playback.",
"url": "https://docs.thewidlarzgroup.com/react-native-video/other/downloading"
}
]
},
"offers": {
"@type": "Offer",
"name": "Enterprise Support",
"description": "Expert support to extend or implement React Native Video capabilities",
"url": "https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=google#Contact",
"seller": {
"@type": "Organization",
"name": "TheWidlarzGroup"
}
}
}

2
docs/next-env.d.ts vendored
View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.

View File

@@ -8,7 +8,7 @@
"build": "bun next build"
},
"dependencies": {
"next": "^13.5.4",
"next": "14.2.20",
"nextra": "^2.13.2",
"nextra-theme-docs": "^2.13.2",
"react": "^18.2.0",

14
docs/pages/_app.mdx Normal file
View File

@@ -0,0 +1,14 @@
import {orbitron} from '../font';
export default function Nextra({Component, pageProps}) {
return (
<>
<style jsx global>{`
:root {
--font-orbitron: ${orbitron.style.fontFamily};
}
`}</style>
<Component {...pageProps} />
</>
);
}

View File

@@ -7,15 +7,39 @@
"type": "separator",
"title": ""
},
"example_apps": {
"title": "Example Apps",
"newWindow": true,
"href": "https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples"
},
"projects": "Useful projects",
"separator_community": {
"type": "separator",
"title": ""
},
"video_offline_sdk": {
"title": "Offline Video SDK",
"newWindow": true,
"href": "https://sdk.thewidlarzgroup.com/offline-video/?utm_source=rnv&utm_medium=docs&utm_campaign=sidebar&utm_id=offline-video-sdk-button"
},
"enterprise_support": {
"title": "Enterprise Support",
"newWindow": true,
"href": "https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs&utm_campaign=navbar&utm_id=enterprise#Contact"
},
"issue_boost": {
"title": "Boost Your Issue",
"newWindow": true,
"href": "https://www.thewidlarzgroup.com/issue-boost/?utm_source=rnv&utm_medium=docs&utm_campaign=sidebar&utm_id=issue-boost-button"
},
"separator_enterprise": {
"type": "separator",
"title": ""
},
"updating": "Updating",
"changelog": {
"title": "Changelog",
"newWindow": true,
"href": "https://github.com/TheWidlarzGroup/react-native-video/blob/master/CHANGELOG.md"
},
"separator_community": {
"type": "separator",
"title": ""
},
"projects": "Useful projects"
}
}

View File

@@ -1,38 +1,190 @@
# Ads
## IMA SDK
`react-native-video` has built-in support for Google IMA SDK for Android and iOS. To enable it please refer to [installation section](/installation)
`react-native-video` includes built-in support for Google IMA SDK on Android and iOS. To enable it, refer to the [installation section](/installation).
The IMA SDK supports two types of ad insertion:
1. **Client-Side Ad Insertion (CSAI)** Ads are inserted client-side using VAST tags
2. **Server-Side Ad Insertion (SSAI)** Server-side ad insertion where ads are stitched into the stream
Both ad types are configured through the unified `ad` property in the source configuration, using the `type` field to specify which mode to use.
---
## Client-Side Ad Insertion (CSAI)
CSAI inserts ads client-side using VAST (Video Ad Serving Template) tags. Ads are requested and played during video playback, with the player handling ad breaks and transitions.
### Usage
To use AVOD, you need to pass `adTagUrl` prop to `Video` component. `adTagUrl` is a VAST uri.
Example:
```
adTagUrl="https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpostoptimizedpodbumper&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator="
```
To use CSAI, configure the `ad` property with `type: 'csai'` and provide an `adTagUrl`. The `adTagUrl` should be a VAST-compliant URI.
> NOTE: Video ads cannot start when you are using the PIP on iOS (more info available at [Google IMA SDK Docs](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/picture_in_picture?hl=en#starting_ads)). If you are using custom controls, you must hide your PIP button when you receive the ```STARTED``` event from ```onReceiveAdEvent``` and show it again when you receive the ```ALL_ADS_COMPLETED``` event.
### Events
To receive events from IMA SDK, you need to pass `onReceiveAdEvent` prop to `Video` component. List of events, you can find [here](https://github.com/TheWidlarzGroup/react-native-video/blob/master/src/types/Ads.ts)
Example:
#### Example:
```jsx
...
onReceiveAdEvent={event => console.log(event)}
...
<Video
source={{
uri: 'https://example.com/video.mp4',
ad: {
type: 'csai',
adTagUrl:
'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpostoptimizedpodbumper&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator=',
},
}}
/>
```
> **Note:** Video ads cannot start when Picture-in-Picture (PiP) mode is active on iOS. More details are available in the [Google IMA SDK Docs](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/picture_in_picture?hl=en#starting_ads). If you are using custom controls, hide the PiP button when receiving the `STARTED` event from `onReceiveAdEvent` and show it again when receiving the `ALL_ADS_COMPLETED` event.
### Events
To receive events from the IMA SDK, pass the `onReceiveAdEvent` prop to the `Video` component. The full list of supported events is available [here](https://github.com/TheWidlarzGroup/react-native-video/blob/master/src/types/Ads.ts).
#### Example:
```jsx
<Video
onReceiveAdEvent={(event) => console.log(event)}
// ... other props
/>
```
### Localization
To change the language of the IMA SDK, you need to pass `adLanguage` prop to `Video` component. List of supported languages, you can find [here](https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/localization#locale-codes)
By default, ios will use system language and android will use `en`
To change the language of the IMA SDK, pass the `adLanguage` prop within the `ad` configuration. The list of supported languages is available [here](https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/localization#locale-codes).
Example:
- By default, **iOS** uses the system language, and **Android** defaults to `en` (English).
#### Example:
```jsx
...
adLanguage="fr"
...
<Video
source={{
uri: 'https://example.com/video.mp4',
ad: {
type: 'csai',
adTagUrl: 'https://example.com/adtag',
adLanguage: 'fr',
},
}}
/>
```
---
## Server-Side Ad Insertion (SSAI)
SSAI (Server-Side Ad Insertion) is a server-side ad insertion solution where ads are stitched into the video stream before it reaches the player. This provides a seamless viewing experience with no playback interruptions, as the stream appears as a single continuous video.
Currently, we support **Google IMA DAI**
SSAI is ideal for:
- Live streaming with ad breaks
- VOD content with dynamic ad insertion
- Scenarios where you want a seamless, uninterrupted viewing experience
### Usage
To use SSAI, configure the `ad` property with `type: 'ssai'` within the `source` prop. SSAI supports both Video On Demand (VOD) and Live streaming.
#### VOD Example:
```jsx
<Video
source={{
ad: {
type: 'ssai',
streamType: 'vod',
contentSourceId: '2548831',
videoId: 'tears-of-steel',
adTagParameters: {
custom_param: 'value',
},
fallbackUri: 'https://example.com/backup-stream.m3u8',
},
}}
/>
```
#### Live Example:
```jsx
<Video
source={{
ad: {
type: 'ssai',
streamType: 'live',
assetKey: 'c-rArva4ShKVIAkNfy6HUQ',
adTagParameters: {
custom_param: 'value',
},
fallbackUri: 'https://example.com/backup-stream.m3u8',
},
}}
/>
```
### Configuration
For VOD streams, you must provide:
- `contentSourceId` The content source ID
- `videoId` The video ID
For Live streams, you must provide:
- `assetKey` The asset key for the live stream
Optional properties:
- `format` Stream format: `'hls'` (default) or `'dash'`. Android only - iOS automatically detects the format.
- `adTagParameters` Custom key-value pairs to pass as ad tag parameters to the IMA SDK. For a list of supported Ad Manager ad tag parameters, see the [Google Ad Manager documentation](https://support.google.com/admanager/answer/7320899?hl=en#npa).
- `fallbackUri` Fallback stream URI. If the SSAI stream fails to load, the player will automatically fall back to this URI
> **Note:** The `streamType` field (`'vod'` or `'live'`) is required to specify the type of SSAI stream.
### Events
SSAI uses the same `onReceiveAdEvent` prop as CSAI to report ad-related events. The full list of supported events is available [here](https://github.com/TheWidlarzGroup/react-native-video/blob/master/src/types/Ads.ts).
#### Example:
```jsx
<Video
source={{
ad: {
type: 'ssai',
streamType: 'vod',
contentSourceId: '2548831',
videoId: 'tears-of-steel',
},
}}
onReceiveAdEvent={(event) => console.log(event)}
// ... other props
/>
```
For more details on ad configuration properties, see the [props documentation](/component/props#ad).
### Fallback Stream
If the SSAI stream fails to load and a `fallbackUri` is provided, the player will automatically fall back to the fallback stream. This ensures playback continuity even when SSAI services are unavailable.
### Example App
For testing and experimenting with SSAI, you can use the `expo-dai` example app located in the `examples/expo-dai` directory. This example app demonstrates SSAI functionality for both VOD and Live streaming scenarios.
### Differences from CSAI
| Feature | CSAI | SSAI |
| ---------------------- | ------------------------------------- | ------------------------------------------ |
| Ad insertion | Client-side | Server-side |
| Playback interruptions | Possible during ad breaks | Seamless, no interruptions |
| Stream format | Original video + separate ad requests | Single unified stream with ads |
| Use case | VOD with pre-defined ad breaks | Live and VOD with server-side ad insertion |
| Configuration | `source.ad` with `type: 'csai'` | `source.ad` with `type: 'ssai'` |
---

View File

@@ -2,56 +2,61 @@ import PlatformsList from '../../components/PlatformsList/PlatformsList.tsx';
# DRM
> **Note:** DRM is not supported on visionOS yet.
## DRM Example
## Provide DRM data (only tested with http/https assets)
We provide a sample implementation in the [example app](https://github.com/TheWidlarzGroup/react-native-video/blob/master/examples/common/DRMExample.tsx) demonstrating how to use DRM with `react-native-video`. Youll need a valid token—visit [our site](https://www.thewidlarzgroup.com/services/free-drm-token-generator-for-video?utm_source=rnv&utm_medium=docs&utm_campaign=drm&utm_id=text) to obtain a **free 24-hour token**.
You can provide some configuration to allow DRM playback.
This feature will disable the use of `TextureView` on Android.
## DRM Offline
DRM object allows this members:
If you need DRM-protected content available offline, our [Offline Video SDK](https://sdk.thewidlarzgroup.com/offline-video/?utm_source=rnv&utm_medium=docs&utm_campaign=drm&utm_id=offline-video-sdk-link) enables downloading, storing, and managing streams with and without DRM. It also handles many edge cases you may encounter over time.
### Prerequisites:
- Use `react-native-video` v6 or v7. If you're still on v5 or lower, [contact us](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs&utm_campaign=drm&utm_id=upgrade-contact#Contact) for assistance.
> Supporting our software kits helps maintain this open-source project. Thank you!
## Providing DRM Data (Tested with HTTP/HTTPS Assets)
You can configure DRM playback by providing a DRM object with the following properties. This feature disables the use of `TextureView` on Android.
### DRM Properties
### `base64Certificate`
<PlatformsList types={['iOS']} />
<PlatformsList types={['iOS', 'visionOS']} />
Type: bool\
Default: false
**Type:** boolean
**Default:** `false`
Whether or not the certificate url returns it on base64.
Indicates whether the certificate URL returns data in Base64 format.
### `certificateUrl`
<PlatformsList types={['iOS']} />
<PlatformsList types={['iOS', 'visionOS']} />
Type: string\
Default: undefined
**Type:** string
**Default:** `undefined`
URL to fetch a valid certificate for FairPlay.
The URL used to fetch a valid certificate for FairPlay.
### `getLicense`
<PlatformsList types={['iOS']} />
<PlatformsList types={['iOS', 'visionOS']} />
Type: function\
Default: undefined
**Type:** function
**Default:** `undefined`
Rather than setting the `licenseServer` url to get the license, you can manually get the license on the JS part, and send the result to the native part to configure FairplayDRM for the stream
Instead of setting `licenseServer`, you can manually acquire the license in JavaScript and send the result to the native module for FairPlay DRM configuration.
`licenseServer` and `headers` will be ignored. You will obtain as argument the `SPC`
(as ASCII string, you will probably need to convert it to base 64) obtained from
your `contentId` + the provided certificate via `objc [loadingRequest streamingContentKeyRequestDataForApp:certificateData
contentIdentifier:contentIdData options:nil error:&spcError]; `
The following parameters are available in `getLicense`:
- `contentId`: The content ID from the DRM object or `loadingRequest.request.url?.host`
- `loadedLicenseUrl`: The URL retrieved from `loadingRequest.request.URL.absoluteString`, starting with `skd://` or `clearkey://`
- `licenseServer`: The URL passed in the DRM object
- `spcString`: The SPC used for DRM validation
Also, you will receive following parameter of getLicense:
* `contentId` contentId if passed to `drm` object or loadingRequest.request.url?.host
* `loadedLicenseUrl` URL defined as `loadingRequest.request.URL.absoluteString`, this url starts with `skd://` or `clearkey://`
* `licenseServer` prop if prop is passed to `drm` object.
* `spcString` the SPC used to validate playback with drm server
You should return a Base64-encoded CKC response, either directly or as a `Promise`.
You should return on this method a `CKC` in Base64, either by just returning it or returning a `Promise` that resolves with the `CKC`.
With this prop you can override the license acquisition flow, as an example:
#### Example:
```js
getLicense: (spcString, contentId, licenseUrl, loadedLicenseUrl) => {
@@ -61,120 +66,118 @@ getLicense: (spcString, contentId, licenseUrl, loadedLicenseUrl) => {
return fetch(`https://license.pallycon.com/ri/licenseManager.do`, {
method: 'POST',
headers: {
'pallycon-customdata-v2':
'd2VpcmRiYXNlNjRzdHJpbmcgOlAgRGFuaWVsIE1hcmnxbyB3YXMgaGVyZQ==',
'pallycon-customdata-v2': 'your-custom-header',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData,
})
.then((response) => response.text())
.then((response) => {
return response;
})
.catch((error) => {
console.error('Error', error);
});
.then((response) => response)
.catch((error) => console.error('Error', error));
};
```
### `contentId`
<PlatformsList types={['iOS']} />
<PlatformsList types={['iOS', 'visionOS']} />
Type: string\
Default: undefined
**Type:** string
**Default:** `undefined`
Specify the content id of the stream, otherwise it will take the host value from `loadingRequest.request.URL.host` (f.e: `skd://testAsset` -> will take `testAsset`)
Sets the content ID for the stream. If not specified, the system uses the host value from `loadingRequest.request.URL.host`.
### `headers`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
Type: Object\
Default: undefined
**Type:** Object
**Default:** `undefined`
You can customize headers send to the licenseServer.
Custom headers for the license server request.
Example:
#### Example:
```js
source={{
uri: 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd',
}}
drm={{
drm: {
type: DRMType.WIDEVINE,
licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense',
headers: {
'X-AxDRM-Message': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImZpcnN0X3BsYXlfZXhwaXJhdGlvbiI6NjAsInBsYXlyZWFkeSI6eyJyZWFsX3RpbWVfZXhwaXJhdGlvbiI6dHJ1ZX0sImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.FAbIiPxX8BHi9RwfzD7Yn-wugU19ghrkBFKsaCPrZmU'
'X-AxDRM-Message': 'your-drm-header',
},
}}
}
```
### `licenseServer`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
Type: string\
Default: false
**Type:** string
**Default:** `undefined`
The URL pointing to the licenseServer that will provide the authorization to play the protected stream.
The license server URL that authorizes protected content playback.
### `multiDrm`
<PlatformsList types={['Android']} />
Type: boolean\
Default: false
Indicates that drm system shall support key rotation, see: https://developer.android.google.cn/media/media3/exoplayer/drm?hl=en#key-rotation
<PlatformsList types={['Android']} />
**Type:** boolean
**Default:** `false`
Indicates whether the DRM system should support key rotation. See [Android Developer Docs](https://developer.android.google.cn/media/media3/exoplayer/drm?hl=en#key-rotation) for more details.
### `type`
<PlatformsList types={['Android', 'iOS']} />
Type: DRMType\
Default: undefined
**Type:** DRMType
**Default:** `undefined`
You can specify the DRM type, either by string or using the exported DRMType enum.
Valid values are, for Android: DRMType.WIDEVINE / DRMType.PLAYREADY / DRMType.CLEARKEY.
for iOS: DRMType.FAIRPLAY
Defines the DRM type:
- **Android:** `DRMType.WIDEVINE`, `DRMType.PLAYREADY`, `DRMType.CLEARKEY`
- **iOS:** `DRMType.FAIRPLAY`
### `localSourceEncryptionKeyScheme`
<PlatformsList types={['iOS']} />
<PlatformsList types={['iOS', 'visionOS']} />
Set the url scheme for stream encryption key for local assets
**Type:** string
Type: String
Sets the URL scheme for stream encryption keys used in local assets.
Example:
#### Example:
```
```js
localSourceEncryptionKeyScheme="my-offline-key"
```
## Common Usage Scenarios
### Send cookies to license server
### Sending Cookies to the License Server
You can send Cookies to the license server via `headers` prop. Example:
You can send cookies using the `headers` prop.
#### Example:
```js
drm: {
type: DRMType.WIDEVINE
type: DRMType.WIDEVINE,
licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense',
headers: {
'Cookie': 'PHPSESSID=etcetc; csrftoken=mytoken; _gat=1; foo=bar'
'Cookie': 'PHPSESSID=your-session-id; csrftoken=mytoken; _gat=1; foo=bar'
},
}
```
### Custom License Acquisition (only iOS for now)
### Custom License Acquisition (iOS Only)
#### Example:
```js
drm: {
type: DRMType.FAIRPLAY,
getLicense: (spcString) => {
const base64spc = Base64.encode(spcString);
return fetch('YOUR LICENSE SERVER HERE', {
return fetch('YOUR_LICENSE_SERVER_URL', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -189,15 +192,12 @@ drm: {
})
.then(response => response.json())
.then((response) => {
if (response && response.getFairplayLicenseResponse
&& response.getFairplayLicenseResponse.ckcResponse) {
if (response?.getFairplayLicenseResponse?.ckcResponse) {
return response.getFairplayLicenseResponse.ckcResponse;
}
throw new Error('No correct response');
throw new Error('No valid response');
})
.catch((error) => {
console.error('CKC error', error);
});
.catch((error) => console.error('CKC error', error));
}
}
```

View File

@@ -2,7 +2,7 @@ import PlatformsList from '../../components/PlatformsList/PlatformsList.tsx';
# Events
This page shows the list of available callbacks to handle player notifications
This page lists all available callbacks for handling player notifications.
## Details
@@ -10,233 +10,240 @@ This page shows the list of available callbacks to handle player notifications
<PlatformsList types={['Android', 'iOS']} />
Callback function that is called when the audio is about to become 'noisy' due to
a change in audio outputs. Typically this is called when audio output is being switched
from an external source like headphones back to the internal speaker. It's a good
idea to pause the media when this happens so the speaker doesn't start blasting sound.
Triggered when audio output changes (e.g., switching from headphones to speakers). It's recommended to pause the media when this event occurs.
Payload: none
**Payload:** _none_
---
### `onAudioFocusChanged`
<PlatformsList types={['Android']} />
Callback function that is called when the audio focus changes. This is called when the audio focus is gained or lost. This is useful for determining if the media should be paused or not.
Called when audio focus is gained or lost.
Payload:
Property | Type | Description
--- | --- | ---
hasAudioFocus | boolean | Boolean indicating whether the media has audio focus
Example:
**Payload:**
| Property | Type | Description |
|---------------|--------|----------------------------------------------|
| hasAudioFocus | boolean | `true` if media has audio focus, `false` otherwise |
**Example:**
```javascript
{
hasAudioFocus: true;
hasAudioFocus: true
}
```
---
### `onAudioTracks`
<PlatformsList types={['Android', 'iOS']} />
Callback function that is called when audio tracks change
Triggered when available audio tracks change.
Payload:
**Payload:** _Array of objects with track details_
An **array** of
| Property | Type | Description |
| -------- | ------- | ---------------------------------------------------------------------------------------------------------- |
|----------|--------|-----------------------------------------------------------------------------|
| index | number | Internal track ID |
| title | string | Descriptive name for the track |
| language | string | 2 letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) representing the language |
| bitrate | number | bitrate of track |
| type | string | Mime type of track |
| selected | boolean | true if track is playing |
Example:
| title | string | Descriptive track name |
| language | string | [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language code |
| bitrate | number | Track bitrate |
| type | string | Track MIME type |
| selected | boolean | `true` if track is currently playing |
**Example:**
```javascript
{
audioTracks: [
{ language: 'es', title: 'Spanish', type: 'audio/mpeg', index: 0, selected: true },
{ language: 'en', title: 'English', type: 'audio/mpeg', index: 1 }
];
]
}
```
---
### `onBandwidthUpdate`
<PlatformsList types={['Android', 'iOS']} />
Callback function that is called when the available bandwidth changes.
Payload:
Called when available bandwidth changes.
**Payload:**
| Property | Type | Description |
| -------- | ------ | ---------------------------------------------- |
| bitrate | number | The estimated bitrate in bits/sec |
| width | number | The width of the video (android only) |
| height | number | The height of the video (android only) |
| trackId | string | The track ID of the video track (android only) |
Example on iOS:
|----------|--------|-----------------------------------------------|
| bitrate | number | Estimated bitrate in bits/sec |
| width | number | Video width (Android only) |
| height | number | Video height (Android only) |
| trackId | string | Video track ID (Android only) |
**Example (iOS):**
```javascript
{
bitrate: 1000000;
bitrate: 1000000
}
```
Example on Android:
**Example (Android):**
```javascript
{
bitrate: 1000000;
width: 1920;
height: 1080;
trackId: 'some-track-id';
bitrate: 1000000,
width: 1920,
height: 1080,
trackId: 'some-track-id'
}
```
Note: On Android, you must set the [reportBandwidth](#reportbandwidth) prop to enable this event. This is due to the high volume of events generated.
> **Note:** On Android, set the [`reportBandwidth`](#reportbandwidth) prop to enable this event.
---
### `onBuffer`
<PlatformsList types={['Android', 'iOS', 'web']} />
Callback function that is called when the player buffers.
Payload:
Triggered when buffering starts or stops.
**Payload:**
| Property | Type | Description |
| ----------- | ------- | ---------------------------------------------- |
| isBuffering | boolean | Boolean indicating whether buffering is active |
Example:
|------------|--------|---------------------------------|
| isBuffering | boolean | `true` if buffering is active |
**Example:**
```javascript
{
isBuffering: true;
isBuffering: true
}
```
---
### `onControlsVisibilityChange`
<PlatformsList types={['Android']} />
Callback function that is called when the controls are hidden or shown. Not possible on iOS.
Payload:
Triggered when the video player controls become visible or hidden.
**Payload:**
| Property | Type | Description |
| ----------- | ------- | ---------------------------------------------- |
| isVisible | boolean | Boolean indicating whether controls are visible |
Example:
|----------|--------|-------------------------------------|
| isVisible | boolean | `true` if controls are visible |
**Example:**
```javascript
{
isVisible: true;
isVisible: true
}
```
---
### `onEnd`
<PlatformsList types={['All']} />
Callback function that is called when the player reaches the end of the media.
Triggered when the media reaches the end.
Payload: none
**Payload:** _none_
---
### `onError`
<PlatformsList types={['All']} />
Callback function that is called when the player experiences a playback error.
Payload:
Called when a playback error occurs.
**Payload:**
| Property | Type | Description |
| -------- | ------ | ------------------------------------------------------------- |
| error | object | Object containing properties with information about the error |
|---------|--------|---------------------------|
| error | object | Error details |
---
### `onExternalPlaybackChange`
<PlatformsList types={['iOS']} />
Callback function that is called when external playback mode for current playing video has changed. Mostly useful when connecting/disconnecting to Apple TV it's called on connection/disconnection.
Payload:
Called when external playback mode changes (e.g., Apple TV connection/disconnection).
**Payload:**
| Property | Type | Description |
| ------------------------ | ------- | ----------------------------------------------------------- |
| isExternalPlaybackActive | boolean | Boolean indicating whether external playback mode is active |
Example:
|-------------------------|--------|--------------------------------------------|
| isExternalPlaybackActive | boolean | `true` if external playback is active |
**Example:**
```javascript
{
isExternalPlaybackActive: true;
isExternalPlaybackActive: true
}
```
---
### `onFullscreenPlayerWillPresent`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
Callback function that is called when the player is about to enter fullscreen mode.
Called before entering fullscreen mode.
Payload: none
**Payload:** _none_
---
### `onFullscreenPlayerDidPresent`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
Callback function that is called when the player has entered fullscreen mode.
Called when fullscreen mode is active.
Payload: none
**Payload:** _none_
---
### `onFullscreenPlayerWillDismiss`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
Callback function that is called when the player is about to exit fullscreen mode.
Called before exiting fullscreen mode.
Payload: none
**Payload:** _none_
---
### `onFullscreenPlayerDidDismiss`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
Callback function that is called when the player has exited fullscreen mode.
Called when fullscreen mode is exited.
Payload: none
**Payload:** _none_
---
### `onLoad`
<PlatformsList types={['All']} />
Callback function that is called when the media is loaded and ready to play.
Triggered when the media is loaded and ready to play.
NOTE: tracks (`audioTracks`, `textTracks` & `videoTracks`) are not available on the web.
Payload:
### Payload:
| Property | Type | Description |
|-------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| currentTime | number | Time in seconds where the media will start |
| duration | number | Length of the media in seconds |
| naturalSize | object | Properties:<br/> _ width - Width in pixels that the video was encoded at<br/> _ height - Height in pixels that the video was encoded at<br/> \* orientation - "portrait", "landscape" or "square" |
| audioTracks | array | An array of audio track info objects with the following properties:<br/> _ index - Index number<br/> _ title - Description of the track<br/> _ language - 2 letter [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) or 3 letter [ISO639-2](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) language code<br/> _ type - Mime type of track |
| textTracks | array | An array of text track info objects with the following properties:<br/> _ index - Index number<br/> _ title - Description of the track<br/> _ language - 2 letter [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) or 3 letter [ISO 639-2](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) language code<br/> _ type - Mime type of track |
| videoTracks | array | An array of video track info objects with the following properties:<br/> _ trackId - ID for the track<br/> _ bitrate - Bit rate in bits per second<br/> _ codecs - Comma separated list of codecs<br/> _ height - Height of the video<br/> \* width - Width of the video |
| naturalSize | object | Properties:<br/> &ensp; width - Width in pixels that the video was encoded at<br/> &ensp; height - Height in pixels that the video was encoded at<br/> &ensp; orientation - "portrait", "landscape" or "square" |
| audioTracks | array | An array of audio track info objects with the following properties:<br/> &ensp; index - Index number<br/> &ensp; title - Description of the track<br/> &ensp; language - 2 letter [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) or 3 letter [ISO639-2](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) language code<br/> &ensp; type - Mime type of track |
| textTracks | array | An array of text track info objects with the following properties:<br/> &ensp; index - Index number<br/> &ensp; title - Description of the track<br/> &ensp; language - 2 letter [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) or 3 letter [ISO 639-2](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) language code<br/> &ensp; type - Mime type of track |
| videoTracks | array | An array of video track info objects with the following properties:<br/> &ensp; trackId - ID for the track<br/> &ensp; bitrate - Bit rate in bits per second<br/> &ensp; codecs - Comma separated list of codecs<br/> &ensp; height - Height of the video<br/> &ensp; width - Width of the video |
| trackId | string | Provide key information about the video track, typically including: `Resolution`, `Bitrate`. |
Example:
**Example:**
```javascript
```js
{
canPlaySlowForward: true,
canPlayReverse: false,
@@ -265,49 +272,51 @@ Example:
{ index: 1, bitrate: 7981888, codecs: "avc1.640028", height: 1080, trackId: "f2-v1-x3", width: 1920 },
{ index: 2, bitrate: 1994979, codecs: "avc1.4d401f", height: 480, trackId: "f3-v1-x3", width: 848 }
],
trackId: "720p 2400kbps"
trackId: "720p 2400kbps",
}
```
> **Note:** `audioTracks`, `textTracks`, and `videoTracks` are not available on the web.
---
### `onLoadStart`
<PlatformsList types={['All']} />
Callback function that is called when the media starts loading.
Payload:
Triggered when media starts loading.
**Payload:**
| Property | Type | Description |
| --------- | ----------- | ---------------------------------------------------------------- |
| isNetwork | boolean | Boolean indicating if the media is being loaded from the network |
| type | string | Type of the media. Not available on Windows |
| uri | string | URI for the media source. Not available on Windows |
Example:
|----------|--------|-------------------------------------|
| isNetwork | boolean | `true` if media is loaded from a network |
| type | string | Media type (not available on Windows) |
| uri | string | Media source URI (not available on Windows) |
**Example:**
```javascript
{
isNetwork: true,
type: '',
uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8'
uri: 'https://example.com/video.mp4'
}
```
---
### `onPlaybackStateChanged`
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
Callback function that is called when the playback state changes.
Payload:
Triggered when playback state changes.
**Payload:**
| Property | Type | Description |
| --------- | ----------- | -------------------------------------------------- |
| isPlaying | boolean | Boolean indicating if the media is playing or not |
| isSeeking | boolean | Boolean indicating if the player is seeking or not |
Example:
|----------|--------|-------------------------------------|
| isPlaying | boolean | `true` if media is playing |
| isSeeking | boolean | `true` if seeking is in progress |
**Example:**
```javascript
{
isPlaying: true,
@@ -315,56 +324,62 @@ Example:
}
```
---
### `onPictureInPictureStatusChanged`
<PlatformsList types={['iOS']} />
<PlatformsList types={['iOS', 'Android', 'web']} />
Callback function that is called when picture in picture becomes active or inactive.
Triggered when Picture-in-Picture (PiP) mode is activated or deactivated.
**Payload:**
| Property | Type | Description |
| -------- | ------- | ------------------------------------------------------- |
| isActive | boolean | Boolean indicating whether picture in picture is active |
Example:
|----------|--------|----------------------------------|
| isActive | boolean | `true` if PiP mode is active |
**Example:**
```javascript
{
isActive: true;
isActive: true
}
```
---
### `onPlaybackRateChange`
<PlatformsList types={['All']} />
Callback function that is called when the rate of playback changes - either paused or starts/resumes.
Triggered when playback speed changes.
**Payload:**
| Property | Type | Description |
| ------------ | ------ | --------------------------------------------------------------------------------------------------------------- |
| playbackRate | number | 0 when playback is paused, 1 when playing at normal speed. Other values when playback is slowed down or sped up |
Example:
|-------------|--------|---------------------------------|
| playbackRate | number | `0` (paused), `1` (normal speed), other values indicate speed changes |
**Example:**
```javascript
{
playbackRate: 0, // indicates paused
playbackRate: 0 // indicates paused
}
```
---
### `onProgress`
<PlatformsList types={['All']} />
Callback function that is called every progressUpdateInterval milliseconds with info about which position the media is currently playing.
Triggered every `progressUpdateInterval` milliseconds, providing information about the current playback position.
**Payload:**
| Property | Type | Description |
| ---------------- | ------ | ------------------------------------------------------------------------------------------------- |
| currentTime | number | Current position in seconds |
| playableDuration | number | Position to where the media can be played to using just the buffer in seconds |
| seekableDuration | number | Position to where the media can be seeked to in seconds. Typically, the total length of the media |
Example:
|----------------|--------|-------------------------------------------------------------------------|
| currentTime | number | Current playback position (seconds) |
| playableDuration | number | Duration that can be played using only the buffer (seconds) |
| seekableDuration | number | Duration that can be seeked to (usually the total length of the media) |
**Example:**
```javascript
{
currentTime: 5.2,
@@ -373,80 +388,82 @@ Example:
}
```
---
### `onReadyForDisplay`
<PlatformsList types={['Android', 'iOS', 'Web']} />
Callback function that is called when the first video frame is ready for display. This is when the poster is removed.
Triggered when the first video frame is ready to be displayed. This is when the poster is removed.
Payload: none
**Payload:** _none_
- iOS: [readyForDisplay](https://developer.apple.com/documentation/avkit/avplayerviewcontroller/1615830-readyfordisplay?language=objc)
- Android [STATE_READY](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#STATE_READY)
- iOS: [`readyForDisplay`](https://developer.apple.com/documentation/avkit/avplayerviewcontroller/1615830-readyfordisplay?language=objc)
- Android: [`STATE_READY`](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#STATE_READY)
---
### `onReceiveAdEvent`
<PlatformsList types={['Android', 'iOS']} />
Callback function that is called when an AdEvent is received from the IMA's SDK.
Triggered when an AdEvent is received from the IMA SDK.
Enum `AdEvent` possible values for [Android](https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdEvent) and [iOS](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/reference/Enums/IMAAdEventType):
<details>
<summary>Events</summary>
<summary>AdEvent</summary>
| Event | Platform | Description |
| -------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AD_BREAK_ENDED` | iOS | Fired the first time each ad break ends. Applications must reenable seeking when this occurs (only used for dynamic ad insertion). |
| `AD_BREAK_READY` | Android, iOS | Fires when an ad rule or a VMAP ad break would have played if autoPlayAdBreaks is false. |
| `AD_BREAK_STARTED` | iOS | Fired first time each ad break begins playback. If an ad break is watched subsequent times this will not be fired. Applications must disable seeking when this occurs (only used for dynamic ad insertion). |
| `AD_BUFFERING` | Android | Fires when the ad has stalled playback to buffer. |
| `AD_CAN_PLAY` | Android | Fires when the ad is ready to play without buffering, either at the beginning of the ad or after buffering completes. |
| `AD_METADATA` | Android | Fires when an ads list is loaded. |
| `AD_PERIOD_ENDED` | iOS | Fired every time the stream switches from advertising or slate to content. This will be fired even when an ad is played a second time or when seeking into an ad (only used for dynamic ad insertion). |
| `AD_PERIOD_STARTED` | iOS | Fired every time the stream switches from content to advertising or slate. This will be fired even when an ad is played a second time or when seeking into an ad (only used for dynamic ad insertion). |
| `AD_PROGRESS` | Android | Fires when the ad's current time value changes. The event `data` will be populated with an AdProgressData object. |
| `ALL_ADS_COMPLETED` | Android, iOS | Fires when the ads manager is done playing all the valid ads in the ads response, or when the response doesn't return any valid ads. |
| `CLICK` | Android, iOS | Fires when the ad is clicked. |
| `COMPLETED` | Android, iOS | Fires when the ad completes playing. |
| `CONTENT_PAUSE_REQUESTED` | Android | Fires when content should be paused. This usually happens right before an ad is about to cover the content. |
| `CONTENT_RESUME_REQUESTED` | Android | Fires when content should be resumed. This usually happens when an ad finishes or collapses. |
| `CUEPOINTS_CHANGED` | iOS | Cuepoints changed for VOD stream (only used for dynamic ad insertion). |
| `DURATION_CHANGE` | Android | Fires when the ad's duration changes. |
| `ERROR` | Android, iOS | Fires when an error occurred while loading the ad and prevent it from playing. |
| `FIRST_QUARTILE` | Android, iOS | Fires when the ad playhead crosses first quartile. |
| `IMPRESSION` | Android | Fires when the impression URL has been pinged. |
| `INTERACTION` | Android | Fires when an ad triggers the interaction callback. Ad interactions contain an interaction ID string in the ad data. |
| `LINEAR_CHANGED` | Android | Fires when the displayed ad changes from linear to nonlinear, or the reverse. |
| `LOADED` | Android, iOS | Fires when ad data is available. |
| `LOG` | Android, iOS | Fires when a non-fatal error is encountered. The user need not take any action since the SDK will continue with the same or next ad playback depending on the error situation. |
| `MIDPOINT` | Android, iOS | Fires when the ad playhead crosses midpoint. |
| `PAUSED` | Android, iOS | Fires when the ad is paused. |
| `RESUMED` | Android, iOS | Fires when the ad is resumed. |
| `SKIPPABLE_STATE_CHANGED` | Android | Fires when the displayed ads skippable state is changed. |
| `SKIPPED` | Android, iOS | Fires when the ad is skipped by the user. |
| `STARTED` | Android, iOS | Fires when the ad starts playing. |
| `STREAM_LOADED` | iOS | Stream request has loaded (only used for dynamic ad insertion). |
| `TAPPED` | iOS | Fires when the ad is tapped. |
| `THIRD_QUARTILE` | Android, iOS | Fires when the ad playhead crosses third quartile. |
| `UNKNOWN` | iOS | An unknown event has fired |
| `USER_CLOSE` | Android | Fires when the ad is closed by the user. |
| `VIDEO_CLICKED` | Android | Fires when the non-clickthrough portion of a video ad is clicked. |
| `VIDEO_ICON_CLICKED` | Android | Fires when a user clicks a video icon. |
| `VOLUME_CHANGED` | Android | Fires when the ad volume has changed. |
| `VOLUME_MUTED` | Android | Fires when the ad volume has been muted. |
| Event | Platform | Description |
| -------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AD_BREAK_ENDED` | iOS | Fired the first time each ad break ends. Applications must reenable seeking when this occurs (only used for dynamic ad insertion). |
| `AD_BREAK_READY` | Android, iOS | Fires when an ad rule or a VMAP ad break would have played if autoPlayAdBreaks is false. |
| `AD_BREAK_STARTED` | iOS | Fired first time each ad break begins playback. If an ad break is watched subsequent times this will not be fired. Applications must disable seeking when this occurs (only used for dynamic ad insertion). |
| `AD_BUFFERING` | Android | Fires when the ad has stalled playback to buffer. |
| `AD_CAN_PLAY` | Android | Fires when the ad is ready to play without buffering, either at the beginning of the ad or after buffering completes. |
| `AD_METADATA` | Android | Fires when an ads list is loaded. |
| `AD_PERIOD_ENDED` | iOS | Fired every time the stream switches from advertising or slate to content. This will be fired even when an ad is played a second time or when seeking into an ad (only used for dynamic ad insertion). |
| `AD_PERIOD_STARTED` | iOS | Fired every time the stream switches from content to advertising or slate. This will be fired even when an ad is played a second time or when seeking into an ad (only used for dynamic ad insertion). |
| `AD_PROGRESS` | Android | Fires when the ad's current time value changes. The event `data` will be populated with an AdProgressData object. |
| `ALL_ADS_COMPLETED` | Android, iOS | Fires when the ads manager is done playing all the valid ads in the ads response, or when the response doesn't return any valid ads. |
| `CLICK` | Android, iOS | Fires when the ad is clicked. |
| `COMPLETED` | Android, iOS | Fires when the ad completes playing. |
| `CONTENT_PAUSE_REQUESTED` | Android | Fires when content should be paused. This usually happens right before an ad is about to cover the content. |
| `CONTENT_RESUME_REQUESTED` | Android | Fires when content should be resumed. This usually happens when an ad finishes or collapses. |
| `CUEPOINTS_CHANGED` | iOS | Cuepoints changed for VOD stream (only used for dynamic ad insertion). |
| `DURATION_CHANGE` | Android | Fires when the ad's duration changes. |
| `ERROR` | Android, iOS | Fires when an error occurred while loading the ad and prevent it from playing. |
| `FIRST_QUARTILE` | Android, iOS | Fires when the ad playhead crosses first quartile. |
| `IMPRESSION` | Android | Fires when the impression URL has been pinged. |
| `INTERACTION` | Android | Fires when an ad triggers the interaction callback. Ad interactions contain an interaction ID string in the ad data. |
| `LINEAR_CHANGED` | Android | Fires when the displayed ad changes from linear to nonlinear, or the reverse. |
| `LOADED` | Android, iOS | Fires when ad data is available. |
| `LOG` | Android, iOS | Fires when a non-fatal error is encountered. The user need not take any action since the SDK will continue with the same or next ad playback depending on the error situation. |
| `MIDPOINT` | Android, iOS | Fires when the ad playhead crosses midpoint. |
| `PAUSED` | Android, iOS | Fires when the ad is paused. |
| `RESUMED` | Android, iOS | Fires when the ad is resumed. |
| `SKIPPABLE_STATE_CHANGED` | Android | Fires when the displayed ads skippable state is changed. |
| `SKIPPED` | Android, iOS | Fires when the ad is skipped by the user. |
| `STARTED` | Android, iOS | Fires when the ad starts playing. |
| `STREAM_LOADED` | iOS | Stream request has loaded (only used for dynamic ad insertion). |
| `TAPPED` | iOS | Fires when the ad is tapped. |
| `THIRD_QUARTILE` | Android, iOS | Fires when the ad playhead crosses third quartile. |
| `UNKNOWN` | iOS | An unknown event has fired |
| `USER_CLOSE` | Android | Fires when the ad is closed by the user. |
| `VIDEO_CLICKED` | Android | Fires when the non-clickthrough portion of a video ad is clicked. |
| `VIDEO_ICON_CLICKED` | Android | Fires when a user clicks a video icon. |
| `VOLUME_CHANGED` | Android | Fires when the ad volume has changed. |
| `VOLUME_MUTED` | Android | Fires when the ad volume has been muted. |
</details>
Payload:
**Payload:**
| Property | Type | Description |
| -------- | ----------------------------------------- | --------------------- |
|----------|-----------------------------------------|---------------------|
| event | AdEvent | The ad event received |
| data | Record&lt;string, string&gt; \| undefined | The ad event data |
Example:
| data | Record&lt;string, string&gt; \| undefined | Additional ad event data |
**Example:**
```json
{
"data": {
@@ -456,173 +473,172 @@ Example:
}
```
---
### `onRestoreUserInterfaceForPictureInPictureStop`
<PlatformsList types={['iOS', 'visionOS']} />
Callback function that corresponds to Apple's [`restoreUserInterfaceForPictureInPictureStopWithCompletionHandler`](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). Call `restoreUserInterfaceForPictureInPictureStopCompleted` inside of this function when done restoring the user interface.
Corresponds to Apple's [`restoreUserInterfaceForPictureInPictureStopWithCompletionHandler`](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc).
Call `restoreUserInterfaceForPictureInPictureStopCompleted` inside this function when the UI is restored.
Payload: none
**Payload:** _none_
---
### `onSeek`
<PlatformsList types={['Android', 'iOS', 'Windows UWP', 'web']} />
Callback function that is called when a seek completes.
Payload:
Triggered when a seek operation completes.
**Payload:**
| Property | Type | Description |
| ----------- | ------ | ------------------------------- |
| currentTime | number | The current time after the seek |
| seekTime | number | The requested time |
Example:
|------------|--------|---------------------------------|
| currentTime | number | Current time after seeking |
| seekTime | number | Requested seek time |
**Example:**
```javascript
{
currentTime: 100.5;
seekTime: 100;
currentTime: 100.5,
seekTime: 100
}
```
Both the currentTime & seekTime are reported because the video player may not seek to the exact requested position in order to improve seek performance.
> **Note:** On iOS, this callback is not reported when native controls are enabled.
Note: on iOS, when controls are enable, this callback is not reported. This is a known limitation.
---
### `onTimedMetadata`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
Callback function that is called when timed metadata becomes available
Payload:
Triggered when timed metadata is available.
**Payload:**
| Property | Type | Description |
| -------- | ----- | ------------------------- |
|----------|------|--------------------------|
| metadata | array | Array of metadata objects |
Example:
**Example:**
```javascript
{
metadata: [
{value: 'Streaming Encoder', identifier: 'TRSN'},
{value: 'Internet Stream', identifier: 'TRSO'},
{value: 'Any Time You Like', identifier: 'TIT2'},
];
{ value: 'Streaming Encoder', identifier: 'TRSN' },
{ value: 'Internet Stream', identifier: 'TRSO' },
{ value: 'Any Time You Like', identifier: 'TIT2' }
]
}
```
---
### `onTextTrackDataChanged`
<PlatformsList types={['Android', 'iOS']} />
Callback function that is called when new subtitle data is available. It provides the actual subtitle content for the current selected text track, if available (mainly WebVTT).
Payload:
Triggered when new subtitle data becomes available.
**Payload:**
| Property | Type | Description |
| ---------------- | -------- | -------------------------------------------------- |
| `subtitleTracks` | `string` | The subtitles text content in a compatible format. |
Example:
|----------------|--------|--------------------------------------------------|
| subtitleTracks | string | The subtitle text content in a compatible format |
**Example:**
```javascript
{
subtitleTracks: "This blade has a dark past.",
subtitleTracks: "This blade has a dark past."
}
```
For details on how to control the visibility of subtitles, see the [subtitleStyle](./props.mdx#subtitleStyle) section.
---
### `onTextTracks`
<PlatformsList types={['Android', 'iOS']} />
Callback function that is called when text tracks change
Payload:
Triggered when available text (subtitle) tracks change.
**Payload:**
| Property | Type | Description |
| -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|----------|--------|--------------------------------------------------------------------------------------------------------------|
| index | number | Internal track ID |
| title | string | Descriptive name for the track |
| language | string | 2 letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) representing the language |
| type | string | Mime type of the track<br/> _ TextTrackType.SRT - SubRip (.srt)<br/> _ TextTrackType.TTML - TTML (.ttml)<br/> \* TextTrackType.VTT - WebVTT (.vtt)<br/>iOS only supports VTT, Android supports all 3 |
| selected | boolean | true if track is playing |
Example:
| title | string | Track name |
| language | string | 2 letter [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language code |
| type | string | Track MIME type (_VTT_, _SRT_, _TTML_) |
| selected | boolean | `true` if the track is currently playing |
**Example:**
```javascript
{
textTracks: [
{
index: 0,
title: 'Any Time You Like',
type: 'srt',
selected: true,
},
];
title: 'English Subtitles',
type: 'vtt',
selected: true
}
]
}
```
---
### `onVideoTracks`
<PlatformsList types={['Android']} />
Callback function that is called when video tracks change
Payload:
Triggered when video tracks change.
**Payload:**
| Property | Type | Description |
| -------- | ------- | --------------------------------------------------------------- |
| index | number | index of the track |
|----------|---------|-----------------------------------|
| index | number | Track index |
| trackId | string | Internal track ID |
| codecs | string | MimeType of codec used for this track |
| width | number | Track width |
| height | number | Track height |
| bitrate | number | Bitrate in bps |
| selected | boolean | true if track is selected for playing |
| rotation | number | 0, 90, 180 or 270 rotation to apply to the track (android only) |
Example:
| codecs | string | Codec type |
| width | number | Video width |
| height | number | Video height |
| bitrate | number | Track bitrate (bps) |
| selected | boolean | `true` if the track is playing |
| rotation | number | Rotation angle (0, 90, 180, 270) |
**Example:**
```javascript
{
videoTracks: [
{
index: O,
trackId: "0",
codecs: 'video/mp4',
index: 0,
trackId: "1",
codecs: "video/mp4",
width: 1920,
height: 1080,
bitrate: 10000,
bitrate: 5000000,
selected: true,
rotation: 0,
},
];
rotation: 0
}
]
}
```
---
### `onVolumeChange`
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
Callback function that is called when the volume of player changes.
Triggered when the player volume changes.
> Note: This event applies to the volume of the player, not the volume of the device.
Payload:
> **Note:** This event applies to the player's volume, not the device's system volume.
**Payload:**
| Property | Type | Description |
| -------- | ------ | ------------------------------------------ |
| volume | number | The volume of the player (between 0 and 1) |
Example:
|----------|--------|---------------------------------|
| volume | number | Volume level (0 to 1) |
**Example:**
```javascript
{
volume: 0.5;
volume: 0.5
}
```

View File

@@ -2,234 +2,336 @@ import PlatformsList from '../../components/PlatformsList/PlatformsList.tsx';
# Methods
This page shows the list of available methods
This page shows the list of available methods.
## Details
### `dismissFullscreenPlayer`
<PlatformsList types={['Android', 'iOS', 'web']} />
`dismissFullscreenPlayer(): Promise<void>`
```tsx
dismissFullscreenPlayer(): Promise<void>
```
Take the player out of fullscreen mode.
Exits fullscreen mode.
> [!WARNING]
> deprecated, use setFullScreen method instead
> **Deprecated:** Use `setFullScreen(false)` instead.
---
### `pause`
<PlatformsList types={['Android', 'iOS', 'web']} />
`pause(): Promise<void>`
```tsx
pause(): Promise<void>
```
Pause the video.
Pauses the video.
---
### `presentFullscreenPlayer`
<PlatformsList types={['Android', 'iOS', 'web']} />
`presentFullscreenPlayer(): Promise<void>`
```tsx
presentFullscreenPlayer(): Promise<void>
```
Put the player in fullscreen mode.
Enters fullscreen mode.
On iOS, this displays the video in a fullscreen view controller with controls.
- On **iOS**, this opens a fullscreen view controller with controls.
- On **Android**, this makes the player fullscreen but requires styling to match screen dimensions.
On Android, this puts the navigation controls in fullscreen mode. It is not a complete fullscreen implementation, so you will still need to apply a style that makes the width and height match your screen dimensions to get a fullscreen video.
> **Deprecated:** Use `setFullScreen(true)` instead.
> [!WARNING]
> deprecated, use setFullScreen method instead
---
### `resume`
<PlatformsList types={['Android', 'iOS', 'web']} />
`resume(): Promise<void>`
```tsx
resume(): Promise<void>
```
Resume the video.
Resumes video playback.
---
### `restoreUserInterfaceForPictureInPictureStopCompleted`
<PlatformsList types={['iOS']} />
`restoreUserInterfaceForPictureInPictureStopCompleted(restored)`
```tsx
restoreUserInterfaceForPictureInPictureStopCompleted(restored)
```
This function corresponds to the completion handler in Apple's [restoreUserInterfaceForPictureInPictureStop](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). IMPORTANT: This function must be called after `onRestoreUserInterfaceForPictureInPictureStop` is called.
Must be called after `onRestoreUserInterfaceForPictureInPictureStop`.
Corresponds to Apple's [`restoreUserInterfaceForPictureInPictureStop`](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc).
---
### `save`
<PlatformsList types={['iOS']} />
`save(): Promise<{ uri: string }>`
```tsx
save(): Promise<{ uri: string }>
```
Save video to your Photos with current filter prop. Returns promise.
Saves the video to the user's **Photos app** with the current filter.
Notes:
#### Notes:
- Supports **MP4** export only.
- Exports to the **cache directory** with a generated UUID filename.
- Requires **internet connection** if the video is not already buffered.
- Video remains in the **Photos app** until manually deleted.
- Works with **cached videos**.
- Currently only supports highest quality export
- Currently only supports MP4 export
- Currently only supports exporting to user's cache directory with a generated UUID filename.
- User will need to remove the saved video through their Photos app
- Works with cached videos as well. (Checkout video-caching example)
- If the video is has not began buffering (e.g. there is no internet connection) then the save function will throw an error.
- If the video is buffering then the save function promise will return after the video has finished buffering and processing.
#### Future improvements:
- Support for **multiple quality options**.
- Support for **more formats**.
- Support for **custom directory and filename**.
Future:
---
- Will support multiple qualities through options
- Will support more formats in the future through options
- Will support custom directory and file name through options
### `enterPictureInPicture`
<PlatformsList types={['Android', 'iOS', 'web']} />
```tsx
enterPictureInPicture()
```
Activates Picture-in-Picture (PiP) mode.
#### Android setup:
For **Expo**, enable PiP in `app.json`:
```json
"plugins": [
[
"react-native-video",
{
"enableAndroidPictureInPicture": true
}
]
]
```
For **Bare React Native**, update `AndroidManifest.xml`:
```xml
<activity
android:name=".MainActivity"
android:supportsPictureInPicture="true">
</activity>
```
> **Note:**
> - On **Android**, entering PiP moves the app to the **background**.
> - On **iOS**, **video ads cannot start** in PiP mode ([Google IMA SDK](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/picture_in_picture?hl=en#starting_ads)).
---
### `exitPictureInPicture`
<PlatformsList types={['Android', 'iOS', 'web']} />
```tsx
exitPictureInPicture()
```
Exits Picture-in-Picture (PiP) mode.
---
### `seek`
<PlatformsList types={['All']} />
`seek(seconds)`
```tsx
seek(seconds: number)
```
Seek to the specified position represented by seconds. seconds is a float value.
Seeks to the specified position (**in seconds**).
`seek()` can only be called after the `onLoad` event has fired. Once completed, the [onSeek](./events#onseek) event will be called.
#### Notes:
- **Must be called after** `onLoad`.
- Triggers the [`onSeek`](./events#onseek) event.
#### Exact seek
#### **iOS Exact Seek:**
```tsx
seek(seconds, tolerance: number)
```
- Default **tolerance**: ±100ms.
- Set `tolerance = 0` for **precise seeking**.
<PlatformsList types={['iOS']} />
By default iOS seeks within 100 milliseconds of the target position. If you need more accuracy, you can use the seek with tolerance method:
`seek(seconds, tolerance)`
tolerance is the max distance in milliseconds from the seconds position that's allowed. Using a more exact tolerance can cause seeks to take longer. If you want to seek exactly, set tolerance to 0.
---
### `setVolume`
<PlatformsList types={['Android', 'iOS', 'web']} />
`setVolume(value): Promise<void>`
```tsx
setVolume(value: number): Promise<void>
```
This function will change the volume exactly like [volume](./props#volume) property. default value and range are the same then.
Changes the **volume** level. Same behavior as the [`volume`](./props#volume) prop.
---
### `getCurrentPosition`
<PlatformsList types={['Android', 'iOS', 'web']} />
`getCurrentPosition(): Promise<number>`
```tsx
getCurrentPosition(): Promise<number>
```
This function retrieves and returns the precise current position of the video playback, measured in seconds.
This function will throw an error if player is not initialized.
Returns the **current playback position** in seconds.
> **Throws an error** if the player is not initialized.
---
### `setSource`
<PlatformsList types={['Android', 'iOS']} />
`setSource(source: ReactVideoSource): Promise<void>`
```tsx
setSource(source: ReactVideoSource): Promise<void>
```
This function will change the source exactly like [source](./props#source) property.
Changing source with this function will overide source provided as props.
Updates the media source **dynamically**.
> **Note:** This **overrides** the `source` prop.
---
### `setFullScreen`
<PlatformsList types={['Android', 'iOS', 'web']} />
`setFullScreen(fullscreen): Promise<void>`
```tsx
setFullScreen(fullscreen: boolean): Promise<void>
```
If you set it to `true`, the player enters fullscreen mode. If you set it to `false`, the player exits fullscreen mode.
Toggles fullscreen mode.
On iOS, this displays the video in a fullscreen view controller with controls.
- `true` → Enters fullscreen.
- `false` → Exits fullscreen.
On Android, this puts the navigation controls in fullscreen mode. It is not a complete fullscreen implementation, so you will still need to apply a style that makes the width and height match your screen dimensions to get a fullscreen video.
---
### `nativeHtmlVideoRef`
<PlatformsList types={['web']} />
A ref to the underlying html video element. This can be used if you need to integrate a 3d party, web only video library (like hls.js, shaka, video.js...).
A **reference to the native HTML `<video>` element**.
Useful for integrating **third-party** video libraries like **hls.js, shaka, video.js, etc.**.
---
### Example Usage
### **Example Usage**
```tsx
const videoRef = useRef<VideoRef>(null);
const someCoolFunctions = async () => {
if (!videoRef.current) {
return;
}
const handleVideoControls = async () => {
if (!videoRef.current) return;
// present or dismiss fullscreen player
// Fullscreen controls
videoRef.current.presentFullscreenPlayer();
videoRef.current.dismissFullscreenPlayer();
// pause or resume the video
// Playback controls
videoRef.current.pause();
videoRef.current.resume();
// save video to your Photos with current filter prop
// Save video
const response = await videoRef.current.save();
const path = response.uri;
console.log('Saved video path:', response.uri);
// seek to the specified position represented by seconds
// Seek to 200s (or with tolerance on iOS)
videoRef.current.seek(200);
// or on iOS you can seek with tolerance
videoRef.current.seek(200, 10);
};
return (
<Video
ref={videoRef}
source={{uri: 'https://www.w3schools.com/html/mov_bbb.mp4'}}
source={{ uri: 'https://www.w3schools.com/html/mov_bbb.mp4' }}
/>
);
```
## Static methods
## Static Methods
### `getWidevineLevel`
<PlatformsList types={['Android']} />
Indicates whether the widevine level supported by device.
```tsx
getWidevineLevel(): Promise<number>
```
Possible values are:
Returns the **Widevine DRM level**:
- 0 - unable to determine widevine support (typically not supported)
- 1, 2, 3 - Widevine level supported
- **0** → Unknown / Not supported.
- **1, 2, 3** → Supported Widevine levels.
---
### `isCodecSupported`
<PlatformsList types={['Android', 'web']} />
Indicates whether the provided codec is supported level supported by device.
```tsx
isCodecSupported(mimetype: string, width: number, height: number): Promise<'hardware' | 'software' | 'unsupported'>
```
parameters:
Checks if the given **video codec** is supported.
- `mimetype`: mime type of codec to query
- `width`, `height`: resolution to query
| Result | Meaning |
|------------|--------------------------------------|
| `hardware` | Hardware decoding supported |
| `software` | Only software decoding available |
| `unsupported` | Codec **not supported** |
Possible results:
- `hardware` - codec is supported by hardware
- `software` - codec is supported by software only
- `unsupported` - codec is not supported
---
### `isHEVCSupported`
<PlatformsList types={['Android']} />
Helper which Indicates whether the provided HEVC/1920\*1080 is supported level supported by device. It uses isCodecSupported internally.
```tsx
isHEVCSupported(): Promise<boolean>
```
### Example Usage
Checks if **HEVC (H.265)** is supported at **1920×1080 resolution**.
> Uses `isCodecSupported` internally.
---
### Static Methods Example Usage
```tsx
import { VideoDecoderProperties } from 'react-native-video';
VideoDecoderProperties.getWidevineLevel().then((level) => {
...
console.log('Widevine Level:', level);
});
VideoDecoderProperties.isCodecSupported('video/hevc', 1920, 1080).then((support) => {
...
console.log('HEVC Support:', support);
});
VideoDecoderProperties.isHEVCSupported().then((support) => {
...
console.log('HEVC 1080p Support:', support);
});
```

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +1,42 @@
# A `<Video>` component for React Native
# A `<Video>` Component for React Native
## About
`react-native-video` is a React Native library that provides a Video component that renders media content such as videos and streams
`react-native-video` is a React Native library that provides a Video component to render media content like videos and streams.
It allows to stream video files (m3u, mpd, mp4, ...) inside your react native application.
It allows you to stream video files (m3u, mpd, mp4, etc.) inside your React Native application.
- Exoplayer for android
- AVplayer for iOS, tvOS and visionOS
- Windows UWP for windows
- HTML5 for web
- ExoPlayer for Android
- AVPlayer for iOS, tvOS, and visionOS
- Windows UWP for Windows
- HTML5 for Web
- Trick mode support
- Subtitles (embeded or side loaded)
- Subtitles (embedded or side-loaded)
- DRM support
- Client side Ads insertion (via google IMA)
- Pip (ios)
- Client-side ad insertion (via Google IMA)
- PiP (Picture-in-Picture)
- Embedded playback controls
- And much more
- And more
The aim of this package is to have a thin and exhaustive control of player.
The goal of this package is to provide lightweight but full control over the player.
## V6.0.0 Information
> ⚠️ **Version 6**: The following documentation refer to features only available through the v6.0.0 releases.
> As major rework has been done in case of doubt, you can still use [version 5.2.x, see documentation](https://github.com/TheWidlarzGroup/react-native-video/blob/v5.2.0/README.md)
> ⚠️ **Version 6**: This documentation covers features available only in v6.0.0 and later.
> If you're unsure or need an older version, you can still use [version 5.2.x](https://github.com/TheWidlarzGroup/react-native-video/blob/v5.2.0/README.md).
Version 6.x requires **react-native >= 0.68.2**
> ⚠️ from **6.0.0-beta.8** requires also **iOS >= 13.0** (default in react-native 0.73)
> ⚠️ From **6.0.0-beta.8**, it also requires **iOS >= 13.0** (default in React Native 0.73).
For older versions of react-native, [please use version 5.x](https://github.com/TheWidlarzGroup/react-native-video/tree/v5.2.0).
For older versions of React Native, [please use version 5.x](https://github.com/TheWidlarzGroup/react-native-video/tree/v5.2.0).
## Usage
```javascript
// Load the module
import Video, { VideoRef } from 'react-native-video';
import Video, {VideoRef} from 'react-native-video';
// Within your render function, assuming you have a file called
// Inside your render function, assuming you have a file called
// "background.mp4" in your project. You can include multiple videos
// on a single screen if you like.
// on a single screen if needed.
const VideoPlayer = () => {
const videoRef = useRef<VideoRef>(null);
@@ -51,14 +50,14 @@ const VideoPlayer = () => {
ref={videoRef}
// Callback when remote video is buffering
onBuffer={onBuffer}
// Callback when video cannot be loaded
// Callback when the video cannot be loaded
onError={onError}
style={styles.backgroundVideo}
/>
)
}
);
};
// Later on in your styles..
// Later in your styles...
var styles = StyleSheet.create({
backgroundVideo: {
position: 'absolute',
@@ -69,3 +68,4 @@ var styles = StyleSheet.create({
},
});
```

View File

@@ -1,6 +1,5 @@
# Installation
Using npm:
```shell
@@ -13,76 +12,99 @@ or using yarn:
yarn add react-native-video
```
Then follow the instructions for your platform to link react-native-video into your project
Then follow the instructions for your platform to link `react-native-video` into your project.
# Specific Platform Installation
# Specific platform installation
<details>
<summary>iOS</summary>
## iOS
### Standard Method
Run `pod install` in the `ios` directory of your project.
⚠️ from version `6.0.0` the minimum iOS version required is `13.0`. For more information see [updating section](updating.md)
⚠️ From version `6.0.0`, the minimum iOS version required is `13.0`. For more information, see the [updating section](updating.md).
### Enable custom feature in podfile file
### Enable Custom Features in the Podfile
Samples available in sample app see [sample pod file](https://github.com/TheWidlarzGroup/react-native-video/blob/9c669a2d8a53df36773fd82ff0917280d0659bc7/examples/basic/ios/Podfile#L34)
Sample configurations are available in the sample app. See the [sample pod file](https://github.com/TheWidlarzGroup/react-native-video/blob/9c669a2d8a53df36773fd82ff0917280d0659bc7/examples/basic/ios/Podfile#L34).
#### Video caching
#### Video Caching
To enable Video caching usage, add following line in your podfile:
([more info here](other/caching.md))
To enable video caching, add the following line to your Podfile: ([more info here](other/caching.md))
```podfile
# enable Video caching
+ $RNVideoUseVideoCaching=true
# Enable Video Caching
$RNVideoUseVideoCaching=true
```
#### Google IMA
Google IMA is the google SDK to support Client Side Ads Integration (CSAI), see [google documentation](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side) for more information.
Google IMA is the SDK for client-side ads integration. See the [Google documentation](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side) for more details.
To enable Google IMA, add the following line to your Podfile:
To enable google IMA usage define add following line in your podfile:
```podfile
$RNVideoUseGoogleIMA=true
```
**If you are using Expo you can use [expo plugin](other/expo.md) for it**
**If you are using Expo, you can use the [Expo plugin](other/expo.md).**
> **Note:** If you are enabling video caching (using `$RNVideoUseVideoCaching`), you must add the following to your `Gemfile`:
>
> ```ruby
> gem "cocoapods-swift-modular-headers"
> ```
>
> Then, install dependencies using:
>
> ```sh
> bundle install
> bundle exec pod install
> ```
>
> This enables Swift modular headers for Swift dependencies.
</details>
<details>
<summary>Android</summary>
## Android
From version >= 6.0.0, your application needs to have kotlin version >= 1.8.0
From version `>= 6.0.0`, your application must use Kotlin version `>= 1.8.0`.
```:
```gradle
buildscript {
...
ext.kotlinVersion = '1.8.0',
ext.kotlinVersion = '1.8.0'
ext.compileSdkVersion = 34
ext.targetSdkVersion = 34
...
}
```
### Enable custom feature in gradle file
### Enable Custom Features in the Gradle File
**If you are using Expo you can use [expo plugin](other/expo.md) for it**
**If you are using Expo, you can use the [Expo plugin](other/expo.md).**
You can disable or enable the following features by setting the following variables in your `android/build.gradle` file:
- `useExoplayerIMA` - Enable Google IMA SDK (Ads support)
You can enable or disable the following features by setting the corresponding variables in your `android/build.gradle` file:
- `useExoplayerIMA` - Enable Google IMA SDK (ads support)
- `useExoplayerRtsp` - Enable RTSP support
- `useExoplayerSmoothStreaming` - Enable SmoothStreaming support
- `useExoplayerDash` - Enable Dash support
- `useExoplayerHls` - Enable HLS support
Each of these features enabled will increase the size of your APK, so only enable the features you need.
By default enabled features are: `useExoplayerSmoothStreaming`, `useExoplayerDash`, `useExoplayerHls`
Each enabled feature increases the APK size, so only enable what you need.
By default, the enabled features are:
- `useExoplayerSmoothStreaming`
- `useExoplayerDash`
- `useExoplayerHls`
Example:
@@ -100,9 +122,10 @@ buildscript {
}
```
See [sample app](https://github.com/TheWidlarzGroup/react-native-video/blob/9c669a2d8a53df36773fd82ff0917280d0659bc7/examples/basic/android/build.gradle#L14C5-L14C5)
See the [sample app](https://github.com/TheWidlarzGroup/react-native-video/blob/9c669a2d8a53df36773fd82ff0917280d0659bc7/examples/basic/android/build.gradle#L14C5-L14C5).
</details>
<details>
<summary>Windows</summary>
@@ -112,81 +135,99 @@ See [sample app](https://github.com/TheWidlarzGroup/react-native-video/blob/9c66
**React Native Windows 0.63 and above**
Autolinking should automatically add react-native-video to your app.
Autolinking should automatically add `react-native-video` to your app.
### Manual Linking
**React Native Windows 0.62**
Make the following additions to the given files manually:
Make the following manual additions:
`windows\myapp.sln`
#### `windows\myapp.sln`
Add the _ReactNativeVideoCPP_ project to your solution (eg. `windows\myapp.sln`):
Add the _ReactNativeVideoCPP_ project to your solution:
1. Open your solution in Visual Studio 2019
2. Right-click Solution icon in Solution Explorer > Add > Existing Project...
3. Select `node_modules\react-native-video\windows\ReactNativeVideoCPP\ReactNativeVideoCPP.vcxproj`
1. Open your solution in Visual Studio 2019.
2. Right-click the Solution icon in Solution Explorer > Add > Existing Project...
3. Select `node_modules\react-native-video\windows\ReactNativeVideoCPP\ReactNativeVideoCPP.vcxproj`.
`windows\myapp\myapp.vcxproj`
#### `windows\myapp\myapp.vcxproj`
Add a reference to _ReactNativeVideoCPP_ to your main application project (eg. `windows\myapp\myapp.vcxproj`):
Add a reference to _ReactNativeVideoCPP_ to your main application project:
1. Open your solution in Visual Studio 2019
2. Right-click main application project > Add > Reference...
3. Check _ReactNativeVideoCPP_ from Solution Projects
1. Open your solution in Visual Studio 2019.
2. Right-click the main application project > Add > Reference...
3. Check _ReactNativeVideoCPP_ from Solution Projects.
`pch.h`
#### `pch.h`
Add `#include "winrt/ReactNativeVideoCPP.h"`.
Add:
`app.cpp`
```cpp
#include "winrt/ReactNativeVideoCPP.h"
```
Add `PackageProviders().Append(winrt::ReactNativeVideoCPP::ReactPackageProvider());` before `InitializeComponent();`.
#### `app.cpp`
Add:
```cpp
PackageProviders().Append(winrt::ReactNativeVideoCPP::ReactPackageProvider());
```
before `InitializeComponent();`.
**React Native Windows 0.61 and below**
Follow the manual linking instructions for React Native Windows 0.62 above, but substitute _ReactNativeVideoCPP61_ for _ReactNativeVideoCPP_.
Follow the manual linking steps for React Native Windows 0.62, but use _ReactNativeVideoCPP61_ instead of _ReactNativeVideoCPP_.
</details>
<details>
<summary>tvOS</summary>
## tvOS
`react-native link react-native-video` doesnt work properly with the tvOS target so we need to add the library manually.
`react-native link react-native-video` does not work properly with the tvOS target, so the library must be added manually.
First select your project in Xcode.
### Steps:
![tvOS step 1](../assets/tvOS-step-1.jpg)
1. Select your project in Xcode.
After that, select the tvOS target of your application and select « General » tab
![tvOS step 1](../assets/tvOS-step-1.jpg)
![tvOS step 2](../assets/tvOS-step-2.jpg)
2. Select the tvOS target of your application and open the "General" tab.
Scroll to « Linked Frameworks and Libraries » and tap on the + button
![tvOS step 2](../assets/tvOS-step-2.jpg)
![tvOS step 3](../assets/tvOS-step-3.jpg)
3. Scroll to "Linked Frameworks and Libraries" and click the `+` button.
Select RCTVideo-tvOS
![tvOS step 3](../assets/tvOS-step-3.jpg)
4. Select `RCTVideo-tvOS`.
![tvOS step 4](../assets/tvOS-step-4.jpg)
![tvOS step 4](../assets/tvOS-step-4.jpg)
</details>
</details>
<details>
<summary>visionOS</summary>
## visionOS
Run `pod install` in the `visionos` directory of your project
Run `pod install` in the `visionos` directory of your project.
</details>
<details>
<summary>web</summary>
<summary>Web</summary>
Nothing to do, everything should work out of the box.
## Web
Note that only basic video support is present, no hls/dash or ads/drm for now.
No additional setup is required.
The Railbird fork uses Shaka Player for browser playback, including HLS and DASH
sources. The native ads and DRM props are not currently integrated with the web
player.
</details>

View File

@@ -1,5 +1,6 @@
{
"caching": "Caching",
"downloading": "Downloading",
"misc": "Misc",
"debug": "Debugging",
"new-arch": "New Architecture",

View File

@@ -1,12 +1,14 @@
# Caching
Caching is supported on `iOS` platforms with a CocoaPods setup, and on `android` using `SimpleCache`.
Caching is supported on `iOS` platforms with a CocoaPods setup and on `Android` using `SimpleCache`.
## Android
Android uses a LRU `SimpleCache` with a variable cache size that can be specified by bufferConfig - cacheSizeMB. This creates a folder named `RNVCache` in the app's `cache` folder. Do note RNV does not yet offer a native call to flush the cache, it can be flushed by clearing the app's cache.
Android uses an LRU `SimpleCache` with a variable cache size, which can be specified by `bufferConfig - cacheSizeMB`. This creates a folder named `RNVCache` inside the app's `cache` directory.
In addition, this resolves RNV6's repeated source URI call problem when looping a video on Android.
Note that `react-native-video` does not currently offer a native method to flush the cache, but it can be cleared by manually clearing the app's cache.
Additionally, this resolves the issue in RNV6 where the source URI was repeatedly called when looping a video on Android.
## iOS
@@ -14,17 +16,33 @@ In addition, this resolves RNV6's repeated source URI call problem when looping
The cache is backed by [SPTPersistentCache](https://github.com/spotify/SPTPersistentCache) and [DVAssetLoaderDelegate](https://github.com/vdugnist/DVAssetLoaderDelegate).
### How Does It Work
### How It Works
The caching is based on the url of the asset.
SPTPersistentCache is a LRU ([Least Recently Used](<https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)>)) cache.
Caching is based on the asset's URL. `SPTPersistentCache` uses an LRU ([Least Recently Used](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU))) caching policy.
### Restrictions
Currently, caching is only supported for URLs that end in a `.mp4`, `.m4v`, or `.mov` extension. In future versions, URLs that end in a query string (e.g. test.mp4?resolution=480p) will be support once dependencies allow access to the `Content-Type` header. At this time, HLS playlists (.m3u8) and videos that sideload text tracks are not supported and will bypass the cache.
Currently, caching is only supported for URLs ending in `.mp4`, `.m4v`, or `.mov`. In future versions, URLs with query strings (e.g., `test.mp4?resolution=480p`) will be supported once dependencies allow access to the `Content-Type` header.
You will also receive warnings in the Xcode logs by using the `debug` mode. So if you are not 100% sure if your video is cached, check your Xcode logs!
At this time, HLS playlists (`.m3u8`) and videos with sideloaded text tracks are not supported and will bypass the cache.
By default files expire after 30 days and the maximum cache size is 100mb.
You will see warnings in the Xcode logs when using `debug` mode. If you're unsure whether your video is cached, check your Xcode logs.
In a future release the cache might have more configurable options.
By default, files expire after 30 days, and the maximum cache size is 100MB.
Future updates may include more configurable caching options.
> **Note:** If you are enabling video caching (using `$RNVideoUseVideoCaching`), you must add the following to your `Gemfile`:
>
> ```ruby
> gem "cocoapods-swift-modular-headers"
> ```
>
> Then, install dependencies using:
>
> ```sh
> bundle install
> bundle exec pod install
> ```
>
> This enables Swift modular headers for Swift dependencies.

View File

@@ -1,20 +1,38 @@
# Debugging
This page describe useful tips for debugging and investigating issue in the package or in your application.
This page provides useful tips for debugging and troubleshooting issues in the package or your application.
## Using the sample app
This repository contains multiple a sample implementation in example folder.
It is always preferable to test behavior on a sample app than in a full app implementation.
The basic sample allow to test a lot of feature.
To use the sample you will need to do steps:
- Clone this repository: ``` git clone git@github.com:TheWidlarzGroup/react-native-video.git```
- Go to root folder and build it. It will generate a transpiled version of the package in lib folder: ```cd react-native-video && yarn && yarn build```
- Go to the sample and install it: ```cd example/basic && yarn install```
- Build it ! for android ```yarn android``` for ios ```cd ios && pod install && cd .. && yarn ios```
## Using the Sample App
This repository contains multiple sample implementations in the `example` folder. It is always preferable to test behavior in a sample app rather than in a full application. The basic sample allows testing of many features.
## HTTP playback doesn't work or Black Screen on Release build (Android)
If your video work on Debug mode, but on Release you see only black screen, please, check the link to your video. If you use 'http' protocol there, you will need to add next string to your AndroidManifest.xml file. [Details here](https://developer.android.com/guide/topics/manifest/application-element#usesCleartextTraffic)
To use the sample app, follow these steps:
- Clone this repository:
```shell
git clone git@github.com:TheWidlarzGroup/react-native-video.git
```
- Navigate to the root folder and build the package. This generates a transpiled version in the `lib` folder:
```shell
cd react-native-video && yarn && yarn build
```
- Navigate to the sample app and install dependencies:
```shell
cd example/basic && yarn install
```
- Build and run the app:
- For Android:
```shell
yarn android
```
- For iOS:
```shell
cd ios && pod install && cd .. && yarn ios
```
## HTTP Playback Doesn't Work or Black Screen on Release Build (Android)
If your video works in Debug mode but shows only a black screen in Release mode, check the URL of your video. If you are using the `http` protocol, you need to add the following line to your `AndroidManifest.xml` file. [More details here](https://developer.android.com/guide/topics/manifest/application-element#usesCleartextTraffic):
```xml
<application
@@ -25,56 +43,56 @@ If your video work on Debug mode, but on Release you see only black screen, plea
## Decoder Issue (Android)
Devices have a maximum of simultaneous possible playback. It means you have reach this limit. Exoplayer returns: 'Unable to instantiate decoder'
Some devices have a maximum number of simultaneous video playbacks. If this limit is reached, ExoPlayer returns an error: `Unable to instantiate decoder`.
**known issue**: This issue happen really often in debug mode.
**Known issue:** This happens frequently in Debug mode.
## You cannot play clean content (all OS)
## Unable to Play Clear Content (All OS)
Here are the steps to consider before opening a ticket in issue tracker
Before opening a ticket, follow these steps:
## Check you can access to remote file
### Check Remote File Access
Ensure you can download to manifest / content file with a browser for example
Ensure you can download the manifest/content file using a browser.
## Check another player can read the content
### Check If Another Player Can Play the Content
Usually clear playback can be read with all Video player. Then you should ensure content can be played without any issue with another player ([VideoLan/VLC](https://www.videolan.org/vlc/) is a good reference implementation)
Clear playback should work with any video player. Test the content with another player, such as [VLC](https://www.videolan.org/vlc/), to confirm it plays without issues.
## You cannot play protected content (all OS)
## Unable to Play Protected Content (All OS)
## Protected content gives error (token error / access forbidden)
### Protected Content Gives an Error (Token Error / Access Forbidden)
If content is protected with an access token or any other http header, ensure you can access to you data with a wget call or a rest client app. You need to provide all needed access token / authentication parameters.
If the content requires an access token or HTTP headers, ensure you can access the data using `wget` or a REST client. Provide all necessary authentication parameters.
## I need to debug network calls but I don't see them in react native debugging tools
## Debugging Network Calls Not Visible in React Native Debugging Tools
This is a react native limitation. React native tools can only see network calls done in JS.
To achieve that, you need to record network trace to ensure communications with server is correct.
[Charles proxy](https://www.charlesproxy.com/) or [Fiddler](https://www.telerik.com/fiddler) are a simple and useful tool to sniff all http/https calls.
With these tool you should be able to analyze what is going on with network. You will see all access to content and DRM, audio / video chunks, ...
This is a React Native limitationReact Native debugging tools only capture network calls made in JavaScript.
Then try to compare exchanges with previous tests you made.
To debug network calls, use tools like:
- [Charles Proxy](https://www.charlesproxy.com/)
- [Fiddler](https://www.telerik.com/fiddler)
## Debug media3: build from media3 source
These tools allow you to sniff all HTTP/HTTPS calls, including access to content, DRM, and audio/video chunks. Compare the request/response patterns with previous tests to diagnose issues.
If you need to use a specific exoplayer version or patch default behavior, you may want to build from media3 source code.
## Debugging Media3: Build from Media3 Source
Building from media3 source is possible. You need to add 2 or 3 things in your app:
If you need to use a specific ExoPlayer version or modify default behavior, you may need to build from the Media3 source code.
### Configure player path
### Configure Player Path
You need to add following lines in settings.gradle to configure your media3 source path:
Add the following lines to `settings.gradle` to configure your Media3 source path:
```gradle
gradle.ext.androidxMediaModulePrefix = 'media-'
apply from: file("../../../../media3/core_settings.gradle")
````
```
Of course, you should replace with media3 source path. Be carefull, you need to use the same version (or version with compatible api) that the package support.
Replace this with the actual Media3 source path. Ensure that you use the same version (or a compatible API version) supported by the package.
### Enable building from source
In your build.gradle file, add following setting:
### Enable Building from Source
In your `build.gradle` file, add the following setting:
```gradle
buildscript {
@@ -86,6 +104,7 @@ buildscript {
}
```
## It's still not working
## Still Not Working?
You can open a ticket or contact us for [premium support](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs&utm_campaign=debugging&utm_id=enterprise#Contact).
You can try to open a ticket or contact us for [premium support](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs#Contact)!

View File

@@ -0,0 +1,62 @@
# Offline Video SDK
## Add Offline Playback to Your React Native App — Fast
The [Offline Video SDK](https://sdk.thewidlarzgroup.com/offline-video/?utm_source=rnv&utm_medium=docs&utm_campaign=downloading&utm_id=offline-video-sdk-link) is a commercial add-on for `react-native-video` (v6 and v7) that enables secure **offline playback** of HLS streams — including support for **DRM**, **multi-audio**, and **subtitles**.
Its built for teams who need a production-ready solution without spending months on in-house development.
Try it free today and ship faster.
➡️ [Start Free Trial](https://sdk.thewidlarzgroup.com/signup?utm_source=rnv&utm_medium=docs&utm_id=downloading_start-trial-offline-video-sdk-1)
---
## 🚀 Key Features
- **Stream Downloading**
Download and store HLS content for offline playback, including full control over asset management.
- **Offline DRM**
Seamlessly supports offline playback of DRM-protected content, with proper rights enforcement and license handling.
- **Multiple Audio Tracks & Subtitles**
Choose which tracks to download (e.g. language, subtitles) — ideal for localized content.
- **Selective Downloads**
Only selected tracks are downloaded by default to optimize storage.
- **DRM License Optimization**
Works efficiently with persistent licenses — no need to re-fetch unless expired.
- **Background Download Management**
Handles queuing, progress tracking, retries, pausing/resuming all out of the box.
- **Pluggable Architecture**
Compatible with your existing player setup — doesnt interfere with other plugins or custom features.
---
## ⚙️ Compatibility & Requirements
- Supports `react-native-video` **v6** and **v7**
---
## 🤝 Integration & Support
You can integrate the SDK yourself using our documentation and [Offline Video Starter Project](https://github.com/TheWidlarzGroup/react-native-offline-video-starter?utm_source=rnv&utm_medium=docs&utm_id=downloading_offline-video-sdk-starter), which includes a ready-to-run example app demonstrating offline playback, multi-audio, subtitles, and DRM setup.
Alternatively, work with our team to accelerate your roadmap.
- 💬 [Contact us for support](mailto:sdk@thewidlarzgroup.com)
- 🧪 [Try the SDK Free Trial](https://sdk.thewidlarzgroup.com/signup?utm_source=rnv&utm_medium=docs&utm_id=downloading_start-trial-offline-video-sdk-2)
- 🔗 [Learn more about features](https://sdk.thewidlarzgroup.com/offline-video?utm_source=rnv&utm_medium=docs&utm_id=downloading_learn-more-offline-video-sdk)
---
## 📄 Licensing & Trials
The Offline Video SDK is distributed under a commercial license.
You can evaluate it for free for 14 days — no credit card required.
Have questions or need help?
📬 [sdk@thewidlarzgroup.com](mailto:sdk@thewidlarzgroup.com)

View File

@@ -1,32 +1,32 @@
# Expo
## Expo plugin
From version `6.3.1`, we have added support for expo plugin. You can configure `react-native-video` properties in `app.json` (or `app.config.json` or `app.config.js`) file.
It's useful when you are using `expo` managed workflow (expo prebuild) as it will automatically configure `react-native-video` properties in native part of the expo project.
## Expo Plugin
Starting from version `6.3.1`, `react-native-video` supports an Expo plugin. You can configure `react-native-video` properties in the `app.json`, `app.config.json`, or `app.config.js` file.
This is particularly useful when using the `Expo` managed workflow (`expo prebuild`), as it automatically sets up `react-native-video` properties in the native part of the Expo project.
### Example Configuration
```json
// app.json
{
{
"name": "my app",
"plugins": [
[
"react-native-video",
{
// ...
"enableNotificationControls": true,
"androidExtensions": {
"useExoplayerRtsp": false,
"useExoplayerSmoothStreaming": false,
"useExoplayerHls": false,
"useExoplayerDash": false,
"useExoplayerDash": false
}
// ...
}
]
]
}
}
```
## Expo Plugin Properties
@@ -38,3 +38,4 @@ It's useful when you are using `expo` managed workflow (expo prebuild) as it wil
| enableADSExtension | boolean | false | Add required changes to use ads extension for video player |
| enableCacheExtension | boolean | false | Add required changes to use cache extension for video player on iOS |
| androidExtensions | object | {} | You can enable/disable extensions as per your requirement - this allow to reduce library size on android |
| enableAndroidPictureInPicture | boolean | false | Apply configs to be able to use Picture-in-picture on android |

View File

@@ -2,54 +2,60 @@
## iOS App Transport Security
- By default, iOS will only load encrypted (https) urls. If you want to load content from an unencrypted (http) source, you will need to modify your Info.plist file and add the following entry:
By default, iOS only allows loading encrypted (`https`) URLs. If you need to load content from an unencrypted (`http`) source, you must modify your `Info.plist` file and add the following entry:
![App Transport Security](../../assets/AppTransportSecuritySetting.png)
For more detailed info check this [article](https://cocoacasts.com/how-to-add-app-transport-security-exception-domains)
</details>
For more details, check this [article](https://cocoacasts.com/how-to-add-app-transport-security-exception-domains).
## Audio Mixing
At some point in the future, react-native-video will include an Audio Manager for configuring how videos mix with other apps playing sounds on the device.
In future versions, `react-native-video` will include an Audio Manager for configuring how videos mix with other audio-playing apps.
On iOS, if you would like to allow other apps to play music over your video component, make the following change:
On iOS, if you want to allow background music from other apps to continue playing over your video component, update your `AppDelegate.m` file:
**AppDelegate.m**
### **AppDelegate.m**
```objective-c
#import <AVFoundation/AVFoundation.h> // import
#import <AVFoundation/AVFoundation.h> // Import the AVFoundation framework
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil]; // allow
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil]; // Allow background audio
...
}
```
You can also use the [ignoreSilentSwitch](#ignoresilentswitch) prop.
</details>
You can also use the [`ignoreSilentSwitch`](#ignoresilentswitch) prop.
## Android Expansion File Usage
Expansions files allow you to ship assets that exceed the 100MB apk size limit and don't need to be updated each time you push an app update.
This only supports mp4 files and they must not be compressed. Example command line for preventing compression:
Expansion files allow you to include assets exceeding the 100MB APK size limit without requiring an update every time you push a new version.
- Only supports `.mp4` files, and they **must not be compressed**.
- Example command to prevent compression:
```bash
zip -r -n .mp4 *.mp4 player.video.example.com
```
### Example Usage in Code:
```javascript
// Within your render function, assuming you have a file called
// "background.mp4" in your expansion file. Just add your main and (if applicable) patch version
<Video source={{uri: "background", mainVer: 1, patchVer: 0}} // Looks for .mp4 file (background.mp4) in the given expansion version.
resizeMode="cover" // Fill the whole screen at aspect ratio.
style={styles.backgroundVideo} />
// Assuming "background.mp4" is included in your expansion file.
<Video
source={{uri: "background", mainVer: 1, patchVer: 0}} // Looks for "background.mp4" in the specified expansion version.
resizeMode="cover" // Fill the whole screen while maintaining aspect ratio.
style={styles.backgroundVideo}
/>
```
## Load files with the RN Asset System
## Load Files with the React Native Asset System
The asset system [introduced in RN `0.14`](http://www.reactnative.com/react-native-v0-14-0-released/) allows loading image resources shared across iOS and Android without touching native code. As of RN `0.31` [the same is true](https://github.com/facebook/react-native/commit/91ff6868a554c4930fd5fda6ba8044dbd56c8374) of mp4 video assets for Android. As of [RN `0.33`](https://github.com/facebook/react-native/releases/tag/v0.33.0) iOS is also supported. Requires `react-native-video@0.9.0`.
The asset system introduced in RN `0.14` allows loading shared image resources across iOS and Android without modifying native code. As of RN `0.31`, the same applies to `.mp4` video assets on Android. From RN `0.33`, iOS support was added. Requires `react-native-video@0.9.0` or later.
### Example:
```javascript
<Video
@@ -57,6 +63,8 @@ The asset system [introduced in RN `0.14`](http://www.reactnative.com/react-nati
/>
```
## Play in background on iOS
## Play in Background on iOS
To enable audio to play in background on iOS the audio session needs to be set to `AVAudioSessionCategoryPlayback`. See [Apple documentation][3] for additional details. (NOTE: there is now a ticket to [expose this as a prop]( https://github.com/react-native-community/react-native-video/issues/310) )
To allow audio playback in the background on iOS, set the audio session to `AVAudioSessionCategoryPlayback`. See the [Apple documentation](https://developer.apple.com/documentation/avfoundation/avaudiosession) for more details.
_(Note: There is an open ticket to [expose this as a prop](https://github.com/react-native-community/react-native-video/issues/310).)_

View File

@@ -1,12 +1,18 @@
# New Architecture
## Fabric
Library currently does not support Fabric. We are working on it. In the meantime, you can use Interop Layer.
The library currently does not support Fabric. We are working on adding support. In the meantime, you can use the Interop Layer.
## Interop Layer
You can use this library on New Architecture by using Interop Layer. <br/> To use Interop Layer you need to have `react-native` >= `0.72.0` & `react-native-video` >= `6.0.0-beta.5`.
For `react-native` < `0.74` you need to add config in `react-native.config.js` file.
You can use this library with the New Architecture by enabling the Interop Layer.
### Requirements:
- `react-native` **>= 0.72.0**
- `react-native-video` **>= 6.0.0-beta.5**
For `react-native` versions **< 0.74**, you need to add the following configuration in the `react-native.config.js` file:
```javascript
module.exports = {
@@ -22,4 +28,5 @@ module.exports = {
```
## Bridgeless Mode
Library currently does not support Bridgeless Mode. We are working on it.
The library currently does not support Bridgeless Mode. We are working on adding support.

View File

@@ -1,115 +1,312 @@
# Plugin (experimental)
# Plugin
Since Version 6.4.0, it is possible to create plugins for analytics management and maybe much more.
A sample plugin is available in the repository in: example/react-native-video-plugin-sample. (important FIXME, put sample link)
Since version `6.4.0`, it is possible to create plugins for analytics management and potentially more.
A sample plugin is available in the repository: [example/react-native-video-plugin-sample](https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples/react-native-video-plugin-sample).
## Concept
## Commercial Plugins
Most of the analytics system which tracks player information (bitrate, errors, ...) can be integrated directly with Exoplayer or AVPlayer handles.
We at The Widlarz Group have created a set of plugins for comprehensive offline video support. If you are interested, check out our [Offline Video SDK](https://sdk.thewidlarzgroup.com/offline-video/?utm_source=rnv&utm_medium=docs&utm_campaign=plugins&utm_id=text). If you need additional plugins (analytics, processing, etc.), let us know.
This plugin system allows none intrusive integration of analytics in the react-native-package. It shall be done in native language (kotlin/swift).
> Using or recommending our commercial software helps support the maintenance of this open-source project. Thank you!
The idea behind this system is to be able to plug an analytics package to react native video without doing any code change (ideally).
## Plugins for Analytics
Following documentation will show on how to create a new plugin for react native video
Most analytics systems that track player data (e.g., bitrate, errors) can be integrated directly with ExoPlayer or AVPlayer.
This plugin system allows for non-intrusive analytics integration with `react-native-video`. It should be implemented in native languages (Kotlin/Swift) to ensure efficiency.
## Warning and consideration
This is an experiental API, it is subject to change. The api with player is very simple but should be flexible enough to implement analytics system. If you need some metadata, you should implement setter in the new package you are creating.
The goal is to enable easy analytics integration without modifying `react-native-video` itself.
As api is flexible, it makes possible to missuse the system. It is necessary to consider the player handle as read-only. If you modify player behavior, we cannot garanty the good behavior of react-native-video package.
## Warnings & Considerations
## General
This is an **experimental API** and may change over time. The API is simple yet flexible enough to implement analytics systems.
If additional metadata is needed, you should implement a setter in your custom package.
First you need to create a new react native package:
````shell
Since the API is flexible, misuse is possible. The player handle should be treated as **read-only**. Modifying player behavior may cause unexpected issues in `react-native-video`.
## General Setup
First, create a new React Native package:
```shell
npx create-react-native-library@latest react-native-video-custom-analytics
````
```
Both android and iOS implementation expose an interface `RNVPlugin`.
Your `react-native-video-custom-analytics` shall implement this interface and register itself as a plugin for react native video.
Both Android and iOS implementations expose an `RNVPlugin` interface.
Your `react-native-video-custom-analytics` package should implement this interface and register itself as a plugin for `react-native-video`.
## Android
There is no special requierement for gradle file.
You need two mandatory action to be able to receive player handle
## Plugin Types
### 1/ Create the plugin
There are two types of plugins you can implement:
First you should instanciate a class which extends `RNVPlugin`.
1. **Base Plugin (`RNVPlugin`)**: For general-purpose plugins that don't need specific player implementation details.
2. **Player-Specific Plugins**:
- `RNVAVPlayerPlugin` for iOS: Provides type-safe access to AVPlayer instances
- `RNVExoplayerPlugin` for Android: Provides type-safe access to ExoPlayer instances
The proposed integration implement `RNVPlugin` directly inside the Module file (`VideoPluginSampleModule`).
Choose the appropriate plugin type based on your needs. If you need direct access to player-specific APIs, use the player-specific plugin classes.
The `RNVPlugin` interface only defines 2 functions, see description here under.
## Android Implementation
### 1. Create the Plugin
You can implement either the base `RNVPlugin` interface or the player-specific `RNVExoplayerPlugin` interface.
#### Base Plugin
```kotlin
/**
class MyAnalyticsPlugin : RNVPlugin {
override fun onInstanceCreated(id: String, player: Any) {
// Handle player creation
}
override fun onInstanceRemoved(id: String, player: Any) {
// Handle player removal
}
}
```
#### ExoPlayer-Specific Plugin
```kotlin
class MyExoPlayerAnalyticsPlugin : RNVExoplayerPlugin {
override fun onInstanceCreated(id: String, player: ExoPlayer) {
// Handle ExoPlayer creation with type-safe access
}
override fun onInstanceRemoved(id: String, player: ExoPlayer) {
// Handle ExoPlayer removal with type-safe access
}
}
```
The `RNVPlugin` interface defines two functions:
```kotlin
/**
* Function called when a new player is created
* @param id: a random string identifying the player
* @param player: the instantiated player reference
*/
fun onInstanceCreated(id: String, player: Any)
/**
fun onInstanceCreated(id: String, player: Any)
/**
* Function called when a player should be destroyed
* when this callback is called, the plugin shall free all
* resources and release all reference to Player object
* @param id: a random string identifying the player
* @param player: the player to release
*/
fun onInstanceRemoved(id: String, player: Any)
````
fun onInstanceRemoved(id: String, player: Any)
```
### 2/ register the plugin
### 2. Register the Plugin
To register this allocated class in the main react native video package you should call following function:
To register the plugin within the main `react-native-video` package, call:
```kotlin
ReactNativeVideoManager.getInstance().registerPlugin(plugin)
```
The proposed integration register the instanciated class in `createNativeModules` entry point.
Your native module can now track Player updates directly from Player reference and report to backend.
In the sample implementation, the plugin is registered in the `createNativeModules` entry point.
## ios
Once registered, your module can track player updates and report analytics data.
### 1/ podspec integration
### Extending Core Functionality via Plugins
Your new module shall be able to access to react-native-video package, then we must declare it as a dependency of the new module you are creating.
In addition to analytics, plugins can also be used to modify or override core behavior of `react-native-video`.
```podfile
s.dependency "react-native-video"
````
This allows native modules to deeply integrate with the playback system - for example:
- replacing the media source factory,
- modifying the media item before playback starts (e.g., injecting stream keys),
- disabling caching dynamically per source.
### 2/ Create the plugin
These capabilities are available through the advanced Android plugin interface: `RNVExoplayerPlugin`.
First you should instanciate a class which extends `RNVPlugin`.
> ⚠️ These extension points are optional — if no plugin provides them, the player behaves exactly as it did before.
The proposed integration implement `RNVPlugin` directly inside the entry point of the module file (`VideoPluginSample`).
---
The `RNVPlugin` interface only defines 2 functions, see description here under.
#### Plugin Extension Points (Android)
```swift
/**
* Function called when a new player is created
* @param player: the instantiated player reference
*/
func onInstanceCreated(player: Any)
/**
* Function called when a player should be destroyed
* when this callback is called, the plugin shall free all
* resources and release all reference to Player object
* @param player: the player to release
*/
func onInstanceRemoved(player: Any)
If your plugin implements `RNVExoplayerPlugin`, you can override the following methods:
##### 1. `overrideMediaItemBuilder`
Allows you to modify the `MediaItem.Builder` before its used. You can inject stream keys, cache keys, or override URIs.
```kotlin
override fun overrideMediaItemBuilder(
source: Source,
mediaItemBuilder: MediaItem.Builder
): MediaItem.Builder? {
// Return modified builder or null to use default
}
```
### 3/ Register the plugin
##### 2. `overrideMediaDataSourceFactory`
To register this allocated class in the main react native video package you should register it by calling this function:
Lets you replace the data source used by ExoPlayer. Useful for implementing read-only cache or request interception.
```kotlin
override fun overrideMediaDataSourceFactory(
source: Source,
mediaDataSourceFactory: DataSource.Factory
): DataSource.Factory? {
// Return your custom factory or null to use default
}
```
##### 3. `overrideMediaSourceFactory`
Allows you to override the default MediaSource.Factory used by ExoPlayer for creating media sources.
Use this if you need to inject a custom media source implementation. If you return null, the default media source factory will be used.
```kotlin
override fun overrideMediaSourceFactory(
source: Source,
mediaSourceFactory: MediaSource.Factory,
mediaDataSourceFactory: DataSource.Factory
): MediaSource.Factory? {
// Return your custom factory or null to use default
}
```
##### 4. `shouldDisableCache`
Enables dynamic disabling of the caching system per source.
```kotlin
override fun shouldDisableCache(source: Source): Boolean {
return true // your own logic
}
```
---
Once implemented, `react-native-video` will automatically invoke these methods for each `<Video />` instance.
## iOS Implementation
### 1. Podspec Integration
Your new module must have access to `react-native-video`. Add it as a dependency in your Podspec file:
```podfile
s.dependency "react-native-video"
```
### 2. Create the Plugin
You can implement either the base `RNVPlugin` class or the player-specific `RNVAVPlayerPlugin` class.
#### Base Plugin
```swift
class MyAnalyticsPlugin: RNVPlugin {
override func onInstanceCreated(id: String, player: Any) {
// Handle player creation
}
override func onInstanceRemoved(id: String, player: Any) {
// Handle player removal
}
}
```
#### AVPlayer-Specific Plugin
```swift
class MyAVPlayerAnalyticsPlugin: RNVAVPlayerPlugin {
override func onInstanceCreated(id: String, player: AVPlayer) {
// Handle AVPlayer creation with type-safe access
}
override func onInstanceRemoved(id: String, player: AVPlayer) {
// Handle AVPlayer removal with type-safe access
}
/// Optionally override the asset used by the player before playback starts
override func overridePlayerAsset(source: VideoSource, asset: AVAsset) async -> OverridePlayerAssetResult? {
// Return a modified asset or nil to use the default
return nil
}
}
```
The `RNVAVPlayerPlugin` class defines several extension points:
```swift
/**
* Function called when a new AVPlayer instance is created
* @param id: a random string identifying the player
* @param player: the instantiated AVPlayer
*/
open func onInstanceCreated(id: String, player: AVPlayer) { /* no-op */ }
/**
* Function called when an AVPlayer instance is being removed
* @param id: a random string identifying the player
* @param player: the AVPlayer to release
*/
open func onInstanceRemoved(id: String, player: AVPlayer) { /* no-op */ }
/**
* Optionally override the asset used by the player before playback starts.
* Allows you to modify or replace the AVAsset before it is used to create the AVPlayerItem.
* Return nil to use the default asset.
*
* @param source: The VideoSource describing the video (uri, type, headers, etc.)
* @param asset: The AVAsset prepared by the player
* @return: OverridePlayerAssetResult if you want to override, or nil to use the default
*/
open func overridePlayerAsset(source: VideoSource, asset: AVAsset) async -> OverridePlayerAssetResult? { nil }
```
##### `OverridePlayerAssetResult` and `OverridePlayerAssetType`
To override the asset, return an `OverridePlayerAssetResult`:
```swift
public struct OverridePlayerAssetResult {
public let type: OverridePlayerAssetType
public let asset: AVAsset
public init(type: OverridePlayerAssetType, asset: AVAsset) {
self.type = type
self.asset = asset
}
}
public enum OverridePlayerAssetType {
case partial // Return a partially modified asset; will go through the default prepare process
case full // Return a fully modified asset; will skip the default prepare process
}
```
- Use `.partial` if you want the asset to continue through the player's normal preparation (e.g., for text tracks or metadata injection).
- Use `.full` if you want to provide a fully prepared asset that will be used as-is for playback.
**Example:**
```swift
override func overridePlayerAsset(source: VideoSource, asset: AVAsset) async -> OverridePlayerAssetResult? {
// Example: Replace the asset URL
let newAsset = AVAsset(url: URL(string: "https://example.com/override.mp4")!)
return Result(type: .full, asset: newAsset)
}
```
> Only one plugin can override the player asset at a time. If multiple plugins implement this, only the first will be used.
### 3. Register the Plugin
To register the plugin in `react-native-video`, call:
```swift
ReactNativeVideoManager.shared.registerPlugin(plugin: plugin)
```
The proposed integration register the instanciated class in file `VideoPluginSample` in the init function:
In the sample implementation, the plugin is registered inside the `VideoPluginSample` file within the `init` function:
```swift
import react_native_video
@@ -122,4 +319,118 @@ override init() {
}
```
Your native module can now track Player updates directly from Player reference and report to backend.
Once registered, your module can track player updates and report analytics data to your backend.
## Custom DRM Manager
You can provide a custom DRM manager through your plugin to handle DRM in a custom way. This is useful when you need to integrate with a specific DRM provider or implement custom DRM logic.
### Android Implementation
#### 1/ Create custom DRM manager
Create a class that implements the `DRMManagerSpec` interface:
```kotlin
class CustomDRMManager : DRMManagerSpec {
@Throws(UnsupportedDrmException::class)
override fun buildDrmSessionManager(uuid: UUID, drmProps: DRMProps): DrmSessionManager? {
// Your custom implementation for building DRM session manager
// Return null if the DRM scheme is not supported
// Throw UnsupportedDrmException if the DRM scheme is invalid
}
}
```
#### 2/ Register DRM manager in your plugin
Implement `getDRMManager()` in your ExoPlayer plugin to provide the custom DRM manager:
```kotlin
class CustomVideoPlugin : RNVExoplayerPlugin {
private val drmManager = CustomDRMManager()
override fun getDRMManager(): DRMManagerSpec? {
return drmManager
}
override fun onInstanceCreated(id: String, player: ExoPlayer) {
// Handle player creation
}
override fun onInstanceRemoved(id: String, player: ExoPlayer) {
// Handle player removal
}
}
```
### iOS Implementation
#### 1/ Create custom DRM manager
Create a class that implements the `DRMManagerSpec` protocol:
```swift
class CustomDRMManager: NSObject, DRMManagerSpec {
func createContentKeyRequest(
asset: AVContentKeyRecipient,
drmProps: DRMParams?,
reactTag: NSNumber?,
onVideoError: RCTDirectEventBlock?,
onGetLicense: RCTDirectEventBlock?
) {
// Initialize content key session and handle key request
}
func handleContentKeyRequest(keyRequest: AVContentKeyRequest) {
// Process the content key request
}
func finishProcessingContentKeyRequest(keyRequest: AVContentKeyRequest, license: Data) throws {
// Finish processing the key request with the obtained license
}
func handleError(_ error: Error, for keyRequest: AVContentKeyRequest) {
// Handle any errors during the DRM process
}
func setJSLicenseResult(license: String, licenseUrl: String) {
// Handle successful license acquisition from JS side
}
func setJSLicenseError(error: String, licenseUrl: String) {
// Handle license acquisition errors from JS side
}
}
```
#### 2/ Register DRM manager in your plugin
Implement `getDRMManager()` in your AVPlayer plugin to provide the custom DRM manager:
```swift
class CustomVideoPlugin: RNVAVPlayerPlugin {
override func getDRMManager() -> DRMManagerSpec? {
return CustomDRMManager()
}
override func onInstanceCreated(id: String, player: AVPlayer) {
// Handle player creation
}
override func onInstanceRemoved(id: String, player: AVPlayer) {
// Handle player removal
}
}
```
### Important notes about DRM managers:
1. Only one plugin can provide a DRM manager at a time. If multiple plugins try to provide DRM managers, only the first one will be used.
2. The custom DRM manager will be used for all video instances in the app.
3. If no custom DRM manager is provided:
- On iOS, the default FairPlay-based implementation will be used
- On Android, the default ExoPlayer DRM implementation will be used
4. The DRM manager must handle all DRM-related functionality:
- On iOS: key requests, license acquisition, and error handling through AVContentKeySession
- On Android: DRM session management and license acquisition through ExoPlayer's DrmSessionManager

View File

@@ -1,11 +1,19 @@
# Useful projects
This page links other open source projects which can be useful for your player implementation. <br>
If you have a project which can be useful for other users, feel free to open a PR to add it here.
# Useful Projects
## UI over react-native-video
- [react-native-video-controls](https://github.com/itsnubix/react-native-video-controls): First reference player UI
- [react-native-media-console](https://github.com/criszz77/react-native-media-console): React-native-video-controls updated and rewritten in typescript
- [react-native-corner-video](https://github.com/Lg0gs/react-native-corner-video): A floating video player
This page lists open-source projects that can be helpful for your player implementation. <br>
If you have a project that could benefit other users, feel free to open a PR to add it here.
## Other tools
- [react-native-track-player](https://github.com/doublesymmetry/react-native-track-player): A toolbox to control player over media session
## Our (TheWidlarzGroup) Libraries
- [react-native-video-player](https://github.com/TheWidlarzGroup/react-native-video-player): Our video player UI library.
- [Offline Video SDK](https://sdk.thewidlarzgroup.com/offline-video?utm_source=rnv&utm_medium=docs&utm_id=projects_offline-video-sdk):
If you're building an app that needs **offline playback** (e.g., downloading HLS videos, subtitles, audio tracks, or DRM-protected content), check out our commercial Offline Video SDK.
It integrates with `react-native-video` and is available with a [free trial](https://sdk.thewidlarzgroup.com/signup?utm_source=rnv&utm_medium=docs&utm_id=projects_start-trial-offline-video-sdk).
To get started quickly, you can clone our [Offline Video Starter Project](https://github.com/TheWidlarzGroup/react-native-offline-video-starter?utm_source=rnv&utm_medium=docs&utm_id=projects_offline-video-starter), which includes a ready-to-run example app demonstrating offline playback, multi-audio, subtitles, and DRM setup.
## Community Libraries
- [react-native-corner-video](https://github.com/Lg0gs/react-native-corner-video): A floating video player.
- [react-native-track-player](https://github.com/doublesymmetry/react-native-track-player): A toolbox for audio playback.
- [react-native-video-controls](https://github.com/itsnubix/react-native-video-controls): A video player UI.
- [react-native-media-console](https://github.com/criszz77/react-native-media-console): An updated version of react-native-video-controls, rewritten in TypeScript.

View File

@@ -1,13 +1,14 @@
# Updating
### Version 6.0.0
## Version 6.0.0
#### iOS
### iOS
##### Min iOS version
From version 6.0.0, the minimum iOS version supported is 13.0. Projects that are using `react-native < 0.73` will need to set the minimum iOS version to 13.0 in the Podfile.
#### Minimum iOS Version
Starting from version 6.0.0, the minimum supported iOS version is 13.0. Projects using `react-native < 0.73` must set the minimum iOS version to 13.0 in the Podfile.
You can do this by adding the following code to your Podfile:
You can do it by adding the following code to your Podfile:
```diff
- platform :ios, min_ios_version_supported
@@ -17,16 +18,16 @@ You can do it by adding the following code to your Podfile:
+ end
```
##### linking
In your project Podfile add support for static dependency linking. This is required to support the new Promises subdependency in the iOS swift conversion.
#### Linking
In your project's Podfile, add support for static dependency linking. This is required to support the new Promises subdependency in the iOS Swift conversion.
Add `use_frameworks! :linkage => :static` just under `platform :ios` in your ios project Podfile.
Add `use_frameworks! :linkage => :static` right below `platform :ios` in your iOS project Podfile.
[See the example ios project for reference](https://github.com/TheWidlarzGroup/react-native-video/blob/master/examples/basic/ios/Podfile#L5)
[See the example iOS project for reference](https://github.com/TheWidlarzGroup/react-native-video/blob/master/examples/basic/ios/Podfile#L5).
##### podspec
#### Podspec
You can remove following lines from your podfile as they are not necessary anymore
You can remove the following lines from your Podfile as they are no longer needed:
```diff
- `pod 'react-native-video', :path => '../node_modules/react-native-video/react-native-video.podspec'`
@@ -34,19 +35,35 @@ You can remove following lines from your podfile as they are not necessary anymo
- `pod 'react-native-video/VideoCaching', :path => '../node_modules/react-native-video/react-native-video.podspec'`
```
If you were previously using VideoCaching, you should $RNVideoUseVideoCaching flag in your podspec, see: [installation section](https://thewidlarzgroup.github.io/react-native-video/installation#video-caching)
If you were previously using VideoCaching, you should set the `$RNVideoUseVideoCaching` flag in your Podspec. See the [installation section](https://docs.thewidlarzgroup.com/react-native-video/installation#video-caching) for details.
#### Android
> **Note:** If you are enabling video caching (using `$RNVideoUseVideoCaching`), you must add the following to your `Gemfile`:
>
> ```ruby
> gem "cocoapods-swift-modular-headers"
> ```
>
> Then, install dependencies using:
>
> ```sh
> bundle install
> bundle exec pod install
> ```
>
> This enables Swift modular headers for Swift dependencies.
If you are already using Exoplayer on V5, you should remove the patch done from **android/settings.gradle**
### Android
If you were using ExoPlayer on V5, remove the patch from **android/settings.gradle**:
```diff
- include ':react-native-video'
- project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer')
``````
```
#### Using App Build Settings
You need to create a `project.ext` section in the top-level `build.gradle` file (not `app/build.gradle`). Fill in the values from the example below using the ones found in your `app/build.gradle` file.
##### Using app build settings
You will need to create a `project.ext` section in the top-level build.gradle file (not app/build.gradle). Fill in the values from the example below using the values found in your app/build.gradle file.
```groovy
// Top-level build file where you can add configuration options common to all sub-projects/modules.
@@ -66,4 +83,5 @@ allprojects {
}
}
```
If you encounter an error `Could not find com.android.support:support-annotations:27.0.0.` reinstall your Android Support Repository.
If you encounter the error `Could not find com.android.support:support-annotations:27.0.0.`, reinstall your Android Support Repository.

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
docs/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,4 +1,6 @@
import React from 'react';
import TWGBadge from './components/TWGBadge/TWGBadge';
import jsonLd from './json-ld.json';
export default {
head: (
@@ -13,7 +15,7 @@ export default {
/>
<meta
name="og:image"
content="https://thewidlarzgroup.github.io/react-native-video/thumbnail.jpg"
content="https://docs.thewidlarzgroup.com/react-native-video/thumbnail.jpg"
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="React Native Video" />
@@ -23,14 +25,29 @@ export default {
/>
<meta
name="twitter:image"
content="https://thewidlarzgroup.github.io/react-native-video/thumbnail.jpg"
content="https://docs.thewidlarzgroup.com/react-native-video/thumbnail.jpg"
/>
<meta name="twitter:image:alt" content="React Native Video" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin />
<link
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400..900&display=swap"
rel="stylesheet"
rel="icon"
type="image/x-icon"
href="https://docs.thewidlarzgroup.com/react-native-video/favicon.ico"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="https://docs.thewidlarzgroup.com/react-native-video/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="https://docs.thewidlarzgroup.com/react-native-video/favicon-16x16.png"
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
/>
</>
),
@@ -39,74 +56,28 @@ export default {
🎬 <strong>Video component</strong> for React Native
</span>
),
faviconGlyph: '🎬',
project: {
link: 'https://github.com/TheWidlarzGroup/react-native-video',
},
docsRepositoryBase:
'https://github.com/TheWidlarzGroup/react-native-video/tree/master/docs/',
main: ({children}) => (
<>
{children}
<TWGBadge visibleOnLarge={false} />
</>
),
toc: {
extraContent: <TWGBadge visibleOnLarge={true} />,
},
footer: {
text: (
<span>
Built with by <strong>React Native Community</strong>
Built with by <strong>TheWidlarzGroup</strong> &{' '}
<strong>React Native Community</strong>
</span>
),
},
toc: {
extraContent: (
<>
<style>{`
:is(html[class~=dark]) .extra-container {
background-color: #87ccef;
}
:is(html[class~=dark]) .extra-text {
color: #171717;
}
:is(html[class~=dark]) .extra-button {
background-color: #171717;
}
.extra-container {
display: flex;
flex-direction: column;
margin-top: 0.5rem;
text-align: center;
background-color: #171717;
padding: 1rem;
gap: 1rem;
border-radius: 0.5rem;
}
.extra-text {
padding-left: 0.5rem;
padding-right: 0.5rem;
font-weight: bold;
color: #fff;
}
.extra-button {
width: 100%;
border: none;
padding: 0.5rem 1rem;
font-weight: 500;
background-color: #f9d85b;
transition: transform 0.3s ease, background-color 0.3s ease;
}
.extra-button:hover {
transform: scale(1.05);
background-color: #fff;
}
`}</style>
<div className="extra-container">
<span className="extra-text">We are TheWidlarzGroup</span>
<a
target="_blank"
href="https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs#Contact"
className="extra-button"
rel="noreferrer">
Premium support
</a>
</div>
</>
),
},
useNextSeoProps() {
return {

View File

@@ -1,6 +0,0 @@
[android]
target = Google Inc.:Google APIs:23
[maven_repositories]
central = https://repo1.maven.org/maven2

View File

@@ -1,2 +0,0 @@
BUNDLE_PATH: "vendor/bundle"
BUNDLE_FORCE_RUBY_PLATFORM: 1

View File

@@ -1,16 +0,0 @@
module.exports = {
root: true,
extends: '@react-native',
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
overrides: [
{
files: ['*.ts', '*.tsx'],
rules: {
'@typescript-eslint/no-shadow': ['error'],
'no-shadow': 'off',
'no-undef': 'off',
},
},
],
};

View File

@@ -1,67 +0,0 @@
# OSX
#
.DS_Store
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
ios/.xcode.env.local
# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
*.hprof
.cxx/
# node.js
#
node_modules/
npm-debug.log
yarn-error.log
# BUCK
buck-out/
\.buckd/
*.keystore
!debug.keystore
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/
**/fastlane/report.xml
**/fastlane/Preview.html
**/fastlane/screenshots
**/fastlane/test_output
# Bundle artifact
*.jsbundle
# Ruby / CocoaPods
/ios/Pods/
/vendor/bundle/
# testing
/coverage

View File

@@ -1,7 +0,0 @@
module.exports = {
arrowParens: 'avoid',
bracketSameLine: true,
bracketSpacing: false,
singleQuote: true,
trailingComma: 'all',
};

View File

@@ -1 +0,0 @@
2.7.5

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,19 +0,0 @@
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* Generated with the TypeScript template
* https://github.com/react-native-community/react-native-template-typescript
*
* @format
*/
import React, {type PropsWithChildren} from 'react';
import {StyleSheet, View} from 'react-native';
import VideoPlayer from './src/VideoPlayer';
const App = () => {
return <VideoPlayer />;
};
export default App;

View File

@@ -1,7 +0,0 @@
source 'https://rubygems.org'
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby ">= 2.6.10"
gem 'cocoapods', '~> 1.13'
gem 'activesupport', '>= 6.1.7.3', '< 7.1.0'

View File

@@ -1,17 +0,0 @@
/**
* @format
*/
import 'react-native';
import React from 'react';
import App from '../App';
// Note: import explicitly to use the types shipped with jest.
import {it} from '@jest/globals';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
it('renders correctly', () => {
renderer.create(<App />);
});

View File

@@ -1,55 +0,0 @@
# To learn about Buck see [Docs](https://buckbuild.com/).
# To run your application with Buck:
# - install Buck
# - `npm start` - to start the packager
# - `cd android`
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
# - `buck install -r android/app` - compile, install and run application
#
load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
lib_deps = []
create_aar_targets(glob(["libs/*.aar"]))
create_jar_targets(glob(["libs/*.jar"]))
android_library(
name = "all-libs",
exported_deps = lib_deps,
)
android_library(
name = "app-code",
srcs = glob([
"src/main/java/**/*.java",
]),
deps = [
":all-libs",
":build_config",
":res",
],
)
android_build_config(
name = "build_config",
package = "net.video.fabricexample",
)
android_resource(
name = "res",
package = "net.video.fabricexample",
res = "src/main/res",
)
android_binary(
name = "app",
keystore = "//android/keystores:debug",
manifest = "src/main/AndroidManifest.xml",
package_type = "debug",
deps = [
":app-code",
],
)

View File

@@ -1,123 +0,0 @@
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
import com.android.build.OutputFile
/**
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.
*/
react {
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '..'
// root = file("../")
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
// reactNativeDir = file("../node-modules/react-native")
// The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
// codegenDir = file("../node_modules/@react-native/codegen")
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
// cliFile = file("../node_modules/react-native/cli.js")
/* Variants */
// The list of variants to that are debuggable. For those we're going to
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
// debuggableVariants = ["liteDebug", "prodDebug"]
/* Bundling */
// A list containing the node command and its flags. Default is just 'node'.
// nodeExecutableAndArgs = ["node"]
//
// The command to run when bundling. By default is 'bundle'
// bundleCommand = "ram-bundle"
//
// The path to the CLI configuration file. Default is empty.
// bundleConfig = file(../rn-cli.config.js)
//
// The name of the generated asset file containing your JS bundle
// bundleAssetName = "MyApplication.android.bundle"
//
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
// entryFile = file("../js/MyApplication.android.js")
//
// A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = []
/* Hermes Commands */
// The hermes compiler command to run. By default it is 'hermesc'
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
}
/**
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
*/
def enableProguardInReleaseBuilds = false
/**
* The preferred build flavor of JavaScriptCore (JSC)
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc:+'
android {
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
namespace "net.video.fabricexample"
defaultConfig {
applicationId "net.video.fabricexample"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
}
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
implementation("com.facebook.react:flipper-integration")
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
}
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)

View File

@@ -1,19 +0,0 @@
"""Helper definitions to glob .aar and .jar targets"""
def create_aar_targets(aarfiles):
for aarfile in aarfiles:
name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")]
lib_deps.append(":" + name)
android_prebuilt_aar(
name = name,
aar = aarfile,
)
def create_jar_targets(jarfiles):
for jarfile in jarfiles:
name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")]
lib_deps.append(":" + name)
prebuilt_jar(
name = name,
binary_jar = jarfile,
)

View File

@@ -1,10 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:

View File

@@ -1,26 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.video.fabricexample">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -1,22 +0,0 @@
package net.video.fabricexample
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
class MainActivity : ReactActivity() {
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "FabricExample"
/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
}

View File

@@ -1,45 +0,0 @@
package net.video.fabricexample
import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.flipper.ReactNativeFlipper
import com.facebook.soloader.SoLoader
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
}
override fun getJSMainModuleName(): String = "index"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
override val reactHost: ReactHost
get() = getDefaultReactHost(this.applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
SoLoader.init(this, false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager)
}
}

Some files were not shown because too many files have changed in this diff Show More