1 Commits

Author SHA1 Message Date
75d4eec823 OnSeekComplete 2024-12-03 11:03:21 -07:00
198 changed files with 6109 additions and 9458 deletions

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&utm_campaign=issue-template&utm_id=sponsorship#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#Contact) - [`hi@thewidlarzgroup.com`](mailto:hi@thewidlarzgroup.com)

View File

@@ -13,7 +13,7 @@ runs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 20.x
node-version: 18.x
- name: Cache dependencies
id: yarn-cache

View File

@@ -25,40 +25,28 @@ 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: (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! 🚀`,
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.`,
MISSING_INFO: (missingFields) => {
return `Hey! 👋
Thanks for the bug report. To help us resolve your issue effectively, we still need some key information:\n\n${missingFields
return `Thank you for your issue report. Please note that the following information is missing or incomplete:\n\n${missingFields
.map((field) => `- ${field.replace('missing-', '')}`)
.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.`;
.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`;
},
OUTDATED_VERSION: (issueVersion, latestVersion) => {
return (
`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.`
`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.`
);
},
};
@@ -126,17 +114,6 @@ 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();
@@ -201,8 +178,7 @@ const handleIssue = async ({github, context}) => {
const handleFeatureRequest = async ({github, context, body, labels}) => {
validateFeatureRequest(body, labels);
const comment = MESSAGE.FEATURE_REQUEST(context.payload.issue.number);
await hidePreviousComments({github, context});
const comment = MESSAGE.FEATURE_REQUEST;
await createComment({github, context, body: comment});
};
@@ -236,8 +212,6 @@ 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});
}
@@ -246,7 +220,7 @@ const handleMissingInformation = async ({github, context, labels}) => {
};
const handleValidReport = async ({github, context, labels}) => {
let comment = MESSAGE.BUG_REPORT(context.payload.issue.number);
let comment = MESSAGE.BUG_REPORT;
const outdatedVersionLabel = Array.from(labels).find((label) =>
label.startsWith('outdated-version'),
@@ -293,15 +267,11 @@ const hidePreviousComments = async ({github, context}) => {
issue_number: context.payload.issue.number,
});
// 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'),
const botComments = comments.data.filter(
(comment) => comment.user.type === 'Bot',
);
for (const comment of unhiddenBotComments) {
for (const comment of botComments) {
// Don't format string - it will broke the markdown
const hiddenBody = `
<details>

View File

@@ -78,7 +78,7 @@ jobs:
-scheme BareExample \
-sdk iphonesimulator \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-destination 'platform=iOS Simulator,name=iPhone 14' \
build \
CODE_SIGNING_ALLOWED=NO | xcpretty"
@@ -142,7 +142,7 @@ jobs:
-scheme BareExample \
-sdk iphonesimulator \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-destination 'platform=iOS Simulator,name=iPhone 14' \
build \
CODE_SIGNING_ALLOWED=NO | xcpretty"
@@ -190,14 +190,11 @@ jobs:
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
run: export RNV_SAMPLE_VIDEO_CACHING=true && pod install
- name: Install Pods
run: export RNV_SAMPLE_VIDEO_CACHING=true && bundle exec pod install
run: export RNV_SAMPLE_VIDEO_CACHING=true && pod install
- name: Install xcpretty
run: gem install xcpretty
@@ -209,6 +206,6 @@ jobs:
-scheme BareExample \
-sdk iphonesimulator \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-destination 'platform=iOS Simulator,name=iPhone 14' \
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@v4
- uses: actions/upload-artifact@v3
with:
name: ktlint-report
path: ./android/build/*.xml

46
.github/workflows/deploy-docs.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
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@v4
- name: Setup
uses: ./.github/actions/setup-bun
with:
working-directory: ./docs
- name: Cache build
uses: actions/cache@v4
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

35
.github/workflows/test-build-docs.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
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@v4
- name: Setup
uses: ./.github/actions/setup-bun
with:
working-directory: ./docs
- name: Cache build
uses: actions/cache@v4
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

4
.gitmodules vendored
View File

@@ -1,4 +0,0 @@
[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

View File

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

View File

@@ -1,224 +1,4 @@
# 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)
@@ -772,7 +552,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

211
README.md
View File

@@ -1,173 +1,66 @@
[![React Native Video Component](./docs/assets/baners/rnv-banner.png)](https://thewidlarzgroup.com/?utm_source=rnv&utm_medium=readme&utm_id=banner)
# react-native-video
🎬 `<Video>` component for React Native
The most battle-tested open-source video player component for React Native with support for DRM, offline playback, HLS/DASH streaming, and more.
## Documentation
documentation is available at [docs.thewidlarzgroup.com/react-native-video/](https://docs.thewidlarzgroup.com/react-native-video/)
## Examples
You can find several examples demonstrating the usage of react-native-video [here](https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples). <br />
These include a [basic](https://github.com/TheWidlarzGroup/react-native-video/blob/master/examples/bare/src/BasicExample.tsx) usage and [DRM example](https://github.com/TheWidlarzGroup/react-native-video/blob/master/examples/bare/src/DRMExample.tsx) (with a [free DRM stream](https://www.thewidlarzgroup.com/services/free-drm-token-generator-for-video?utm_source=drm&utm_medium=code)).
## 🔍 Features
## Usage
- 📱 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
```javascript
// Load the module
import Video, {VideoRef} from 'react-native-video';
// 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.
## ✨ Project Status
const VideoPlayer = () => {
const videoRef = useRef<VideoRef>(null);
const background = require('./background.mp4');
| 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
```
### Usage
```tsx
import Video from 'react-native-video';
export default () => (
return (
<Video
source={{ uri: 'https://www.w3schools.com/html/mov_bbb.mp4' }}
style={{ width: '100%', aspectRatio: 16 / 9 }}
controls
// 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}
/>
);
)
}
// Later on in your styles..
var styles = StyleSheet.create({
backgroundVideo: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
},
});
```
---
## Community support
We have an discord server where you can ask questions and get help. [Join the discord server](https://discord.gg/WXuM4Tgb9X)
## 🧩 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" />
## 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>
</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,9 +7,6 @@ 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++) {
@@ -38,11 +35,6 @@ 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")
}
}
}
@@ -142,11 +134,6 @@ 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,10 +1,10 @@
RNVideo_kotlinVersion=1.8.0
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_minSdkVersion=23
RNVideo_targetSdkVersion=34
RNVideo_compileSdkVersion=34
RNVideo_ndkversion=26.1.10909125
RNVideo_buildToolsVersion=34.0.0
RNVideo_media3Version=1.4.1
RNVideo_useExoplayerIMA=false
RNVideo_useExoplayerRtsp=false
RNVideo_useExoplayerSmoothStreaming=true

View File

@@ -1,65 +0,0 @@
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

@@ -1,26 +0,0 @@
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

@@ -4,98 +4,38 @@ import android.net.Uri
import android.text.TextUtils
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 &&
contentSourceId == other.contentSourceId &&
videoId == other.videoId &&
assetKey == other.assetKey &&
format == other.format &&
adTagParameters == other.adTagParameters &&
fallbackUri == other.fallbackUri
adLanguage == other.adLanguage
)
}
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()
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)) {
if (TextUtils.isEmpty(uriString)) {
adsProps.adTagUrl = null
} else {
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,7 +20,6 @@ class BufferConfig {
var maxHeapAllocationPercent = BufferConfigPropUnsetDouble
var minBackBufferMemoryReservePercent = BufferConfigPropUnsetDouble
var minBufferMemoryReservePercent = BufferConfigPropUnsetDouble
var initialBitrate = BufferConfigPropUnsetInt
var live: Live = Live()
@@ -37,7 +36,6 @@ class BufferConfig {
maxHeapAllocationPercent == other.maxHeapAllocationPercent &&
minBackBufferMemoryReservePercent == other.minBackBufferMemoryReservePercent &&
minBufferMemoryReservePercent == other.minBufferMemoryReservePercent &&
initialBitrate == other.initialBitrate &&
live == other.live
)
}
@@ -93,7 +91,6 @@ class BufferConfig {
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
@@ -121,7 +118,6 @@ 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

@@ -37,10 +37,10 @@ data class CMCDProps(
return (0 until array.size()).mapNotNull { i ->
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")
val key = item.getString("key")
val value = when (item.getType("value")) {
ReadableType.Number -> item.getDouble("value")
ReadableType.String -> item.getString("value")
else -> null
}

View File

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

View File

@@ -5,6 +5,7 @@ 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
@@ -89,7 +90,7 @@ class Source {
*/
var sideLoadedTextTracks: SideLoadedTextTrackList? = null
override fun hashCode(): Int = Objects.hash(uriString, uri, startPositionMs, cropStartMs, cropEndMs, extension, metadata, headers, adsProps)
override fun hashCode(): Int = Objects.hash(uriString, uri, startPositionMs, cropStartMs, cropEndMs, extension, metadata, headers)
/** return true if this and src are equals */
override fun equals(other: Any?): Boolean {
@@ -211,21 +212,27 @@ class Source {
fun parse(src: ReadableMap?, context: Context): Source {
val source = Source()
if (src == null) return source
safeGetString(src, PROP_SRC_URI, null)
?.takeIf { it.isNotBlank() }
?.let { uriString ->
var uri = Uri.parse(uriString)
if (!isValidScheme(uri.scheme)) {
uri = getUriFromAssetId(context, uriString) ?: return 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
}
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
}
}
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)
@@ -248,8 +255,8 @@ class Source {
if (propSrcHeadersArray.size() > 0) {
for (i in 0 until propSrcHeadersArray.size()) {
val current = propSrcHeadersArray.getMap(i)
val key = current?.getString("key")
val value = current?.getString("value")
val key = if (current.hasKey("key")) current.getString("key") else null
val value = if (current.hasKey("value")) current.getString("value") else null
if (key != null && value != null) {
source.headers[key] = value
}
@@ -257,7 +264,7 @@ class Source {
}
}
source.metadata = Metadata.parse(safeGetMap(src, PROP_SRC_METADATA))
}
return source
}

View File

@@ -43,8 +43,7 @@ enum class EventTypes(val eventName: String) {
EVENT_TEXT_TRACK_DATA_CHANGED("onTextTrackDataChanged"),
EVENT_VIDEO_TRACKS("onVideoTracks"),
EVENT_ON_RECEIVE_AD_EVENT("onReceiveAdEvent"),
EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED("onPictureInPictureStatusChanged");
EVENT_ON_RECEIVE_AD_EVENT("onReceiveAdEvent");
companion object {
fun toMap() =
@@ -73,7 +72,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, seekTime: Long) -> Unit
lateinit var onVideoSeekComplete: (currentPosition: Long) -> Unit
lateinit var onVideoEnd: () -> Unit
lateinit var onVideoFullscreenPlayerWillPresent: () -> Unit
lateinit var onVideoFullscreenPlayerDidPresent: () -> Unit
@@ -93,7 +92,6 @@ 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)
@@ -142,30 +140,6 @@ 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)
}
)
}
}
)
}
)
}
)
}
@@ -202,11 +176,9 @@ class VideoEventEmitter {
putDouble("seekTime", seekTime / 1000.0)
}
}
onVideoSeekComplete = { currentPosition, seekTime ->
onVideoSeekComplete = { currentPosition ->
event.dispatch(EventTypes.EVENT_SEEK_COMPLETE) {
putDouble("currentTime", currentPosition / 1000.0)
putDouble("seekTime", seekTime / 1000.0)
putInt("target", view.id)
}
}
onVideoEnd = {
@@ -313,25 +285,15 @@ 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(VideoCustomEvent(surfaceId, viewId, event, paramsSetter))
dispatcher.dispatchEvent(object : Event<Event<*>>(surfaceId, viewId) {
override fun getEventName() = "top${event.eventName.removePrefix("on")}"
override fun getEventData() = Arguments.createMap().apply(paramsSetter ?: {})
})
}
private fun audioTracksToArray(audioTracks: java.util.ArrayList<Track>?): WritableArray =

View File

@@ -1,59 +0,0 @@
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

@@ -1,18 +0,0 @@
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,26 +5,13 @@ import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
class DefaultReactExoplayerConfig(private val context: Context, override var initialBitrate: Long? = null) : ReactExoplayerConfig {
private var bandWidthMeter: DefaultBandwidthMeter = createBandwidthMeter(initialBitrate)
class DefaultReactExoplayerConfig(context: Context) : ReactExoplayerConfig {
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,269 +1,275 @@
package com.brentvatne.exoplayer
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue
import android.view.Gravity
import android.view.SurfaceView
import android.view.TextureView
import android.view.View
import android.view.View.MeasureSpec
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.media3.common.AdViewProvider
import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.common.Timeline
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.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerView
import androidx.media3.ui.SubtitleView
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 @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
FrameLayout(context, attrs, defStyleAttr) {
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? = null
val isPlaying: Boolean
get() = player != null && player?.isPlaying == true
@ViewType.ViewType
private var viewType = ViewType.VIEW_TYPE_SURFACE
private var hideShutterView = false
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 {
// Add PlayerView with explicit layout parameters
val playerViewLayoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
addView(playerView, playerViewLayoutParams)
componentListener = ComponentListener()
// 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)
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)
layout.addView(shutterView, 1, layoutParams)
if (localStyle.subtitlesFollowVideo) {
layout.addView(subtitleLayout, layoutParams)
}
fun setPlayer(player: ExoPlayer?) {
val currentPlayer = playerView.player
if (currentPlayer != null) {
currentPlayer.removeListener(playerListener)
addViewInLayout(layout, 0, aspectRatioParams)
if (!localStyle.subtitlesFollowVideo) {
addViewInLayout(subtitleLayout, 1, layoutParams)
}
}
playerView.player = player
private fun clearVideoView() {
when (val view = surfaceView) {
is TextureView -> player?.clearVideoTextureView(view)
if (player != null) {
player.addListener(playerListener)
is SurfaceView -> player?.clearVideoSurfaceView(view)
// Apply pending resize mode if we have one
pendingResizeMode?.let { resizeMode ->
playerView.resizeMode = resizeMode
else -> {
Log.w(
"clearVideoView",
"Unexpected surfaceView type: ${surfaceView?.javaClass?.name}"
)
}
}
}
fun getPlayerView(): PlayerView = playerView
private fun setVideoView() {
when (val view = surfaceView) {
is TextureView -> player?.setVideoTextureView(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
is SurfaceView -> player?.setVideoSurfaceView(view)
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) {
playerView.subtitleView?.let { subtitleView ->
// Reset to defaults
subtitleView.setUserDefaultStyle()
subtitleView.setUserDefaultTextSize()
// ensure we reset subtitle style before reapplying it
subtitleLayout.setUserDefaultStyle()
subtitleLayout.setUserDefaultTextSize()
// Apply custom styling
if (style.fontSize > 0) {
subtitleView.setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, style.fontSize.toFloat())
subtitleLayout.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, style.fontSize.toFloat())
}
subtitleView.setPadding(
subtitleLayout.setPadding(
style.paddingLeft,
style.paddingTop,
style.paddingRight,
style.paddingTop,
style.paddingBottom
)
if (style.opacity != 0.0f) {
subtitleView.alpha = style.opacity
subtitleView.visibility = android.view.View.VISIBLE
subtitleLayout.alpha = style.opacity
subtitleLayout.visibility = View.VISIBLE
} else {
subtitleView.visibility = android.view.View.GONE
subtitleLayout.visibility = 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) {
playerView.setShutterBackgroundColor(color)
shutterView.setBackgroundColor(color)
}
fun updateSurfaceView(viewType: Int) {
// TODO: Implement proper surface type switching if needed
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)
}
val isPlaying: Boolean
get() = playerView.player?.isPlaying ?: false
ViewType.VIEW_TYPE_TEXTURE -> {
if (surfaceView !is TextureView) {
surfaceView = TextureView(context)
viewNeedRefresh = true
}
// Support opacity properly:
(surfaceView as TextureView).isOpaque = false
}
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
else -> {
DebugLog.wtf(TAG, "Unexpected texture view type: $viewType")
}
}
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()
}
}
if (viewNeedRefresh) {
surfaceView?.layoutParams = layoutParams
fun showController() {
playerView.showController()
if (layout.getChildAt(0) != null) {
layout.removeViewAt(0)
}
layout.addView(surfaceView, 0, layoutParams)
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()
if (this.player != null) {
setVideoView()
}
}
}
companion object {
private const val TAG = "ExoPlayerView"
var adsShown = false
fun showAds() {
if (!adsShown) {
adOverlayFrameLayout = FrameLayout(context)
layout.addView(adOverlayFrameLayout, layoutParams)
adsShown = true
}
}
fun hideAds() {
if (adsShown) {
layout.removeView(adOverlayFrameLayout)
adOverlayFrameLayout = null
adsShown = false
}
}
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)
}
}
/**
* 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]).
* Sets the resize mode which can be of value {@link ResizeMode.Mode}
*
* @param resizeMode The resize mode.
*/
private val layoutRunnable = Runnable {
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 {
measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
@@ -271,19 +277,58 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
layout(left, top, right, bottom)
}
override fun requestLayout() {
super.requestLayout()
// Post a second layout pass so the ExoPlayer internal views get correct bounds.
post(layoutRunnable)
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 onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
fun invalidateAspectRatio() {
// Resetting aspect ratio will force layout refresh on next video size changed
layout.invalidateAspectRatio()
}
if (changed) {
pendingResizeMode?.let { resizeMode ->
playerView.resizeMode = resizeMode
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)
}
}
override fun onRenderedFirstFrame() {
shutterView.visibility = INVISIBLE
}
override fun onTracksChanged(tracks: Tracks) {
updateForCurrentTrackSelections(tracks)
}
}
companion object {
private const val TAG = "ExoPlayerView"
}
}

View File

@@ -5,11 +5,12 @@ 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
@@ -89,6 +90,7 @@ class FullScreenPlayerView(
parent?.removeView(exoPlayerView)
containerView.addView(exoPlayerView, generateDefaultLayoutParams())
playerControlView?.let {
updateFullscreenButton(playerControlView, true)
parent?.removeView(it)
containerView.addView(it, generateDefaultLayoutParams())
}
@@ -101,6 +103,7 @@ class FullScreenPlayerView(
containerView.removeView(exoPlayerView)
parent?.addView(exoPlayerView, generateDefaultLayoutParams())
playerControlView?.let {
updateFullscreenButton(playerControlView, false)
containerView.removeView(it)
parent?.addView(it, generateDefaultLayoutParams())
}
@@ -122,14 +125,6 @@ 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
@@ -137,6 +132,20 @@ 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) {
@@ -208,7 +217,13 @@ class FullScreenPlayerView(
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
)
}
// Note: Live container adjustment is no longer needed since we're using PlayerView's built-in controls
// PlayerView handles UI adjustments automatically
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
}
}
}
}

View File

@@ -1,213 +0,0 @@
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

@@ -1,98 +0,0 @@
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,6 +7,4 @@ interface ReactExoplayerConfig {
fun buildLoadErrorHandlingPolicy(minLoadRetryCount: Int): LoadErrorHandlingPolicy
var disableDisconnectError: Boolean
val bandwidthMeter: DefaultBandwidthMeter
var initialBitrate: Long?
fun setInitialBitrate(bitrate: Long)
}

View File

@@ -32,7 +32,6 @@ 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"
@@ -52,6 +51,7 @@ 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"
@@ -69,7 +69,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
override fun onDropViewInstance(view: ReactExoplayerView) {
view.cleanUpResources()
view.exitPictureInPictureMode()
ReactNativeVideoManager.getInstance().unregisterView(this)
}
@@ -155,11 +154,6 @@ 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))
@@ -226,6 +220,11 @@ 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)

View File

@@ -20,7 +20,6 @@ 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()
@@ -64,9 +63,7 @@ class VideoPlaybackService : MediaSessionService() {
mediaSessionsList[player] = mediaSession
addSession(mediaSession)
val notificationId = player.hashCode()
startForeground(notificationId, buildNotification(mediaSession))
startForeground(mediaSession.player.hashCode(), buildNotification(mediaSession))
}
fun unregisterPlayer(player: ExoPlayer) {
@@ -227,30 +224,7 @@ 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")
@@ -275,7 +249,6 @@ 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,8 +1,7 @@
package com.brentvatne.react
/**
* 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
* Plugin interface definition
*/
interface RNVPlugin {
/**

View File

@@ -1,17 +1,11 @@
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.DRMManagerSpec
import com.brentvatne.exoplayer.RNVExoplayerPlugin
import com.brentvatne.exoplayer.ReactExoplayerViewManager
/**
* 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 {
@@ -29,14 +23,13 @@ class ReactNativeVideoManager : RNVPlugin {
}
}
private val pluginList = ArrayList<RNVPlugin>()
private var customDRMManager: DRMManagerSpec? = null
private var instanceList: ArrayList<Any> = ArrayList()
private var instanceList: ArrayList<ReactExoplayerViewManager> = ArrayList()
private var pluginList: ArrayList<RNVPlugin> = ArrayList()
/**
* register a new ReactExoplayerViewManager in the managed list
*/
fun registerView(newInstance: Any) {
fun registerView(newInstance: ReactExoplayerViewManager) {
if (instanceList.size > 2) {
DebugLog.d(TAG, "multiple Video displayed ?")
}
@@ -46,7 +39,7 @@ class ReactNativeVideoManager : RNVPlugin {
/**
* unregister existing ReactExoplayerViewManager in the managed list
*/
fun unregisterView(newInstance: Any) {
fun unregisterView(newInstance: ReactExoplayerViewManager) {
instanceList.remove(newInstance)
}
@@ -55,8 +48,7 @@ class ReactNativeVideoManager : RNVPlugin {
*/
fun registerPlugin(plugin: RNVPlugin) {
pluginList.add(plugin)
maybeRegisterExoplayerPlugin(plugin)
return
}
/**
@@ -64,11 +56,9 @@ class ReactNativeVideoManager : RNVPlugin {
*/
fun unregisterPlugin(plugin: RNVPlugin) {
pluginList.remove(plugin)
maybeUnregisterExoplayerPlugin(plugin)
return
}
// ----------------------- Generic RNV plugin methods -----------------------
override fun onInstanceCreated(id: String, player: Any) {
pluginList.forEach { it.onInstanceCreated(id, player) }
}
@@ -76,83 +66,4 @@ 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,20 +65,6 @@ 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

@@ -1,72 +0,0 @@
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/player_overlay_color"
android:background="@color/midnight_black"
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="player_overlay_color">#00000000</color>
<color name="midnight_black">#CC000000</color>
<color name="white">#FFFFFF</color>
<color name="red">#FF0000</color>
</resources>

View File

@@ -22,8 +22,4 @@
<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,5 +1,3 @@
node_modules/
out/
.next/
public/llms.txt
public/llms-full.txt

View File

@@ -1,3 +1,15 @@
# react-native-video-docs
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.
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
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 491 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

View File

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

View File

@@ -1,62 +0,0 @@
.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

@@ -1,27 +0,0 @@
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;

View File

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

View File

@@ -1,56 +0,0 @@
{
"@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/pages/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

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

View File

@@ -1,14 +0,0 @@
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,39 +7,20 @@
"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": ""
},
"example_apps": {
"title": "Example Apps",
"newWindow": true,
"href": "https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples"
},
"projects": "Useful projects"
}

View File

@@ -1,190 +1,38 @@
# Ads
## IMA SDK
`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.
`react-native-video` has built-in support for Google IMA SDK for Android and iOS. To enable it please refer to [installation section](/installation)
### Usage
To use AVOD, you need to pass `adTagUrl` prop to `Video` component. `adTagUrl` is a VAST uri.
To use CSAI, configure the `ad` property with `type: 'csai'` and provide an `adTagUrl`. The `adTagUrl` should be a VAST-compliant URI.
#### Example:
```jsx
<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=',
},
}}
/>
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="
```
> **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.
> 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)
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:
Example:
```jsx
<Video
onReceiveAdEvent={(event) => console.log(event)}
// ... other props
/>
...
onReceiveAdEvent={event => console.log(event)}
...
```
### 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)
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).
By default, ios will use system language and android will use `en`
- By default, **iOS** uses the system language, and **Android** defaults to `en` (English).
#### Example:
Example:
```jsx
<Video
source={{
uri: 'https://example.com/video.mp4',
ad: {
type: 'csai',
adTagUrl: 'https://example.com/adtag',
adLanguage: 'fr',
},
}}
/>
...
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

@@ -4,59 +4,57 @@ import PlatformsList from '../../components/PlatformsList/PlatformsList.tsx';
## DRM Example
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**.
We have available example for DRM usage in the [example app](https://github.com/TheWidlarzGroup/react-native-video/blob/master/examples/bare/src/DRMExample.tsx).
To get token needed for DRM playback you can go to [our site](https://www.thewidlarzgroup.com/services/free-drm-token-generator-for-video?utm_source=drm&utm_medium=docs) and get it.
## DRM Offline
## Provide DRM data (only tested with http/https assets)
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.
You can provide some configuration to allow DRM playback.
This feature will disable the use of `TextureView` on Android.
### 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
DRM object allows this members:
### `base64Certificate`
<PlatformsList types={['iOS', 'visionOS']} />
**Type:** boolean
**Default:** `false`
Type: bool\
Default: false
Indicates whether the certificate URL returns data in Base64 format.
Whether or not the certificate url returns it on base64.
### `certificateUrl`
<PlatformsList types={['iOS', 'visionOS']} />
**Type:** string
**Default:** `undefined`
Type: string\
Default: undefined
The URL used to fetch a valid certificate for FairPlay.
URL to fetch a valid certificate for FairPlay.
### `getLicense`
<PlatformsList types={['iOS', 'visionOS']} />
**Type:** function
**Default:** `undefined`
Type: function\
Default: undefined
Instead of setting `licenseServer`, you can manually acquire the license in JavaScript and send the result to the native module for FairPlay DRM configuration.
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
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
`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]; `
You should return a Base64-encoded CKC response, either directly or as a `Promise`.
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
#### Example:
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:
```js
getLicense: (spcString, contentId, licenseUrl, loadedLicenseUrl) => {
@@ -66,14 +64,19 @@ getLicense: (spcString, contentId, licenseUrl, loadedLicenseUrl) => {
return fetch(`https://license.pallycon.com/ri/licenseManager.do`, {
method: 'POST',
headers: {
'pallycon-customdata-v2': 'your-custom-header',
'pallycon-customdata-v2':
'd2VpcmRiYXNlNjRzdHJpbmcgOlAgRGFuaWVsIE1hcmnxbyB3YXMgaGVyZQ==',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData,
})
.then((response) => response.text())
.then((response) => response)
.catch((error) => console.error('Error', error));
.then((response) => {
return response;
})
.catch((error) => {
console.error('Error', error);
});
};
```
@@ -81,103 +84,100 @@ getLicense: (spcString, contentId, licenseUrl, loadedLicenseUrl) => {
<PlatformsList types={['iOS', 'visionOS']} />
**Type:** string
**Default:** `undefined`
Type: string\
Default: undefined
Sets the content ID for the stream. If not specified, the system uses the host value from `loadingRequest.request.URL.host`.
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`)
### `headers`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
**Type:** Object
**Default:** `undefined`
Type: Object\
Default: undefined
Custom headers for the license server request.
You can customize headers send to the licenseServer.
#### Example:
Example:
```js
drm: {
source={{
uri: 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd',
}}
drm={{
type: DRMType.WIDEVINE,
licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense',
headers: {
'X-AxDRM-Message': 'your-drm-header',
'X-AxDRM-Message': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImZpcnN0X3BsYXlfZXhwaXJhdGlvbiI6NjAsInBsYXlyZWFkeSI6eyJyZWFsX3RpbWVfZXhwaXJhdGlvbiI6dHJ1ZX0sImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.FAbIiPxX8BHi9RwfzD7Yn-wugU19ghrkBFKsaCPrZmU'
},
}
}}
```
### `licenseServer`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
**Type:** string
**Default:** `undefined`
Type: string\
Default: false
The license server URL that authorizes protected content playback.
The URL pointing to the licenseServer that will provide the authorization to play the protected stream.
### `multiDrm`
<PlatformsList types={['Android']} />
Type: boolean\
Default: false
**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.
Indicates that drm system shall support key rotation, see: https://developer.android.google.cn/media/media3/exoplayer/drm?hl=en#key-rotation
### `type`
<PlatformsList types={['Android', 'iOS']} />
**Type:** DRMType
**Default:** `undefined`
Type: DRMType\
Default: undefined
Defines the DRM type:
- **Android:** `DRMType.WIDEVINE`, `DRMType.PLAYREADY`, `DRMType.CLEARKEY`
- **iOS:** `DRMType.FAIRPLAY`
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
### `localSourceEncryptionKeyScheme`
<PlatformsList types={['iOS', 'visionOS']} />
**Type:** string
Set the url scheme for stream encryption key for local assets
Sets the URL scheme for stream encryption keys used in local assets.
Type: String
#### Example:
Example:
```js
```
localSourceEncryptionKeyScheme="my-offline-key"
```
## Common Usage Scenarios
### Sending Cookies to the License Server
### Send cookies to license server
You can send cookies using the `headers` prop.
#### Example:
You can send Cookies to the license server via `headers` prop. Example:
```js
drm: {
type: DRMType.WIDEVINE,
type: DRMType.WIDEVINE
licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense',
headers: {
'Cookie': 'PHPSESSID=your-session-id; csrftoken=mytoken; _gat=1; foo=bar'
'Cookie': 'PHPSESSID=etcetc; csrftoken=mytoken; _gat=1; foo=bar'
},
}
```
### Custom License Acquisition (iOS Only)
#### Example:
### Custom License Acquisition (only iOS for now)
```js
drm: {
type: DRMType.FAIRPLAY,
getLicense: (spcString) => {
const base64spc = Base64.encode(spcString);
return fetch('YOUR_LICENSE_SERVER_URL', {
return fetch('YOUR LICENSE SERVER HERE', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -192,12 +192,15 @@ drm: {
})
.then(response => response.json())
.then((response) => {
if (response?.getFairplayLicenseResponse?.ckcResponse) {
if (response && response.getFairplayLicenseResponse
&& response.getFairplayLicenseResponse.ckcResponse) {
return response.getFairplayLicenseResponse.ckcResponse;
}
throw new Error('No valid response');
throw new Error('No correct 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 lists all available callbacks for handling player notifications.
This page shows the list of available callbacks to handle player notifications
## Details
@@ -10,240 +10,233 @@ This page lists all available callbacks for handling player notifications.
<PlatformsList types={['Android', 'iOS']} />
Triggered when audio output changes (e.g., switching from headphones to speakers). It's recommended to pause the media when this event occurs.
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.
**Payload:** _none_
---
Payload: none
### `onAudioFocusChanged`
<PlatformsList types={['Android']} />
Called when audio focus is gained or lost.
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.
**Payload:**
| Property | Type | Description |
|---------------|--------|----------------------------------------------|
| hasAudioFocus | boolean | `true` if media has audio focus, `false` otherwise |
Payload:
Property | Type | Description
--- | --- | ---
hasAudioFocus | boolean | Boolean indicating whether the media has audio focus
Example:
**Example:**
```javascript
{
hasAudioFocus: true
hasAudioFocus: true;
}
```
---
### `onAudioTracks`
<PlatformsList types={['Android', 'iOS']} />
Triggered when available audio tracks change.
Callback function that is called when audio tracks change
**Payload:** _Array of objects with track details_
Payload:
An **array** of
| Property | Type | Description |
|----------|--------|-----------------------------------------------------------------------------|
| -------- | ------- | ---------------------------------------------------------------------------------------------------------- |
| index | number | Internal track ID |
| 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 |
| 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:
**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']} />
Called when available bandwidth changes.
Callback function that is called when the available bandwidth changes.
Payload:
**Payload:**
| Property | Type | Description |
|----------|--------|-----------------------------------------------|
| 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) |
| -------- | ------ | ---------------------------------------------- |
| 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:
**Example (iOS):**
```javascript
{
bitrate: 1000000
bitrate: 1000000;
}
```
**Example (Android):**
Example on 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, set the [`reportBandwidth`](#reportbandwidth) prop to enable this event.
---
Note: On Android, you must set the [reportBandwidth](#reportbandwidth) prop to enable this event. This is due to the high volume of events generated.
### `onBuffer`
<PlatformsList types={['Android', 'iOS', 'web']} />
Triggered when buffering starts or stops.
Callback function that is called when the player buffers.
Payload:
**Payload:**
| Property | Type | Description |
|------------|--------|---------------------------------|
| isBuffering | boolean | `true` if buffering is active |
| ----------- | ------- | ---------------------------------------------- |
| isBuffering | boolean | Boolean indicating whether buffering is active |
Example:
**Example:**
```javascript
{
isBuffering: true
isBuffering: true;
}
```
---
### `onControlsVisibilityChange`
<PlatformsList types={['Android']} />
Triggered when the video player controls become visible or hidden.
Callback function that is called when the controls are hidden or shown. Not possible on iOS.
Payload:
**Payload:**
| Property | Type | Description |
|----------|--------|-------------------------------------|
| isVisible | boolean | `true` if controls are visible |
| ----------- | ------- | ---------------------------------------------- |
| isVisible | boolean | Boolean indicating whether controls are visible |
Example:
**Example:**
```javascript
{
isVisible: true
isVisible: true;
}
```
---
### `onEnd`
<PlatformsList types={['All']} />
Triggered when the media reaches the end.
Callback function that is called when the player reaches the end of the media.
**Payload:** _none_
---
Payload: none
### `onError`
<PlatformsList types={['All']} />
Called when a playback error occurs.
Callback function that is called when the player experiences a playback error.
Payload:
**Payload:**
| Property | Type | Description |
|---------|--------|---------------------------|
| error | object | Error details |
---
| -------- | ------ | ------------------------------------------------------------- |
| error | object | Object containing properties with information about the error |
### `onExternalPlaybackChange`
<PlatformsList types={['iOS']} />
Called when external playback mode changes (e.g., Apple TV connection/disconnection).
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:
**Payload:**
| Property | Type | Description |
|-------------------------|--------|--------------------------------------------|
| isExternalPlaybackActive | boolean | `true` if external playback is active |
| ------------------------ | ------- | ----------------------------------------------------------- |
| isExternalPlaybackActive | boolean | Boolean indicating whether external playback mode is active |
Example:
**Example:**
```javascript
{
isExternalPlaybackActive: true
isExternalPlaybackActive: true;
}
```
---
### `onFullscreenPlayerWillPresent`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
Called before entering fullscreen mode.
Callback function that is called when the player is about to enter fullscreen mode.
**Payload:** _none_
---
Payload: none
### `onFullscreenPlayerDidPresent`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
Called when fullscreen mode is active.
Callback function that is called when the player has entered fullscreen mode.
**Payload:** _none_
---
Payload: none
### `onFullscreenPlayerWillDismiss`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
Called before exiting fullscreen mode.
Callback function that is called when the player is about to exit fullscreen mode.
**Payload:** _none_
---
Payload: none
### `onFullscreenPlayerDidDismiss`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
Called when fullscreen mode is exited.
Callback function that is called when the player has exited fullscreen mode.
**Payload:** _none_
---
Payload: none
### `onLoad`
<PlatformsList types={['All']} />
Triggered when the media is loaded and ready to play.
Callback function that is called when the media is loaded and ready to play.
### Payload:
NOTE: tracks (`audioTracks`, `textTracks` & `videoTracks`) are not available on the web.
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/> &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 |
| 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 |
| trackId | string | Provide key information about the video track, typically including: `Resolution`, `Bitrate`. |
**Example:**
Example:
```js
```javascript
{
canPlaySlowForward: true,
canPlayReverse: false,
@@ -272,51 +265,49 @@ Triggered when the media is loaded and ready to play.
{ 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']} />
Triggered when media starts loading.
Callback function that is called when the media starts loading.
Payload:
**Payload:**
| Property | Type | Description |
|----------|--------|-------------------------------------|
| 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) |
| --------- | ----------- | ---------------------------------------------------------------- |
| 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:
**Example:**
```javascript
{
isNetwork: true,
type: '',
uri: 'https://example.com/video.mp4'
uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8'
}
```
---
### `onPlaybackStateChanged`
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
Triggered when playback state changes.
Callback function that is called when the playback state changes.
Payload:
**Payload:**
| Property | Type | Description |
|----------|--------|-------------------------------------|
| isPlaying | boolean | `true` if media is playing |
| isSeeking | boolean | `true` if seeking is in progress |
| --------- | ----------- | -------------------------------------------------- |
| isPlaying | boolean | Boolean indicating if the media is playing or not |
| isSeeking | boolean | Boolean indicating if the player is seeking or not |
Example:
**Example:**
```javascript
{
isPlaying: true,
@@ -324,62 +315,56 @@ Triggered when playback state changes.
}
```
---
### `onPictureInPictureStatusChanged`
<PlatformsList types={['iOS', 'Android', 'web']} />
<PlatformsList types={['iOS']} />
Triggered when Picture-in-Picture (PiP) mode is activated or deactivated.
Callback function that is called when picture in picture becomes active or inactive.
**Payload:**
| Property | Type | Description |
|----------|--------|----------------------------------|
| isActive | boolean | `true` if PiP mode is active |
| -------- | ------- | ------------------------------------------------------- |
| isActive | boolean | Boolean indicating whether picture in picture is active |
Example:
**Example:**
```javascript
{
isActive: true
isActive: true;
}
```
---
### `onPlaybackRateChange`
<PlatformsList types={['All']} />
Triggered when playback speed changes.
Callback function that is called when the rate of playback changes - either paused or starts/resumes.
**Payload:**
| Property | Type | Description |
|-------------|--------|---------------------------------|
| playbackRate | number | `0` (paused), `1` (normal speed), other values indicate speed changes |
| ------------ | ------ | --------------------------------------------------------------------------------------------------------------- |
| playbackRate | number | 0 when playback is paused, 1 when playing at normal speed. Other values when playback is slowed down or sped up |
Example:
**Example:**
```javascript
{
playbackRate: 0 // indicates paused
playbackRate: 0, // indicates paused
}
```
---
### `onProgress`
<PlatformsList types={['All']} />
Triggered every `progressUpdateInterval` milliseconds, providing information about the current playback position.
Callback function that is called every progressUpdateInterval milliseconds with info about which position the media is currently playing.
**Payload:**
| Property | Type | Description |
|----------------|--------|-------------------------------------------------------------------------|
| 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) |
| ---------------- | ------ | ------------------------------------------------------------------------------------------------- |
| 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:
**Example:**
```javascript
{
currentTime: 5.2,
@@ -388,82 +373,80 @@ Triggered every `progressUpdateInterval` milliseconds, providing information abo
}
```
---
### `onReadyForDisplay`
<PlatformsList types={['Android', 'iOS', 'Web']} />
Triggered when the first video frame is ready to be displayed. This is when the poster is removed.
Callback function that is called when the first video frame is ready for display. 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']} />
Triggered when an AdEvent is received from the IMA SDK.
Callback function that is called when an AdEvent is received from the IMA's 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>AdEvent</summary>
<summary>Events</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:**
| Property | Type | Description |
|----------|-----------------------------------------|---------------------|
| event | AdEvent | The ad event received |
| data | Record&lt;string, string&gt; \| undefined | Additional ad event data |
Payload:
| Property | Type | Description |
| -------- | ----------------------------------------- | --------------------- |
| event | AdEvent | The ad event received |
| data | Record&lt;string, string&gt; \| undefined | The ad event data |
Example:
**Example:**
```json
{
"data": {
@@ -473,172 +456,173 @@ Enum `AdEvent` possible values for [Android](https://developers.google.com/inter
}
```
---
### `onRestoreUserInterfaceForPictureInPictureStop`
<PlatformsList types={['iOS', 'visionOS']} />
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.
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.
**Payload:** _none_
---
Payload: none
### `onSeek`
<PlatformsList types={['Android', 'iOS', 'Windows UWP', 'web']} />
Triggered when a seek operation completes.
Callback function that is called when a seek completes.
Payload:
**Payload:**
| Property | Type | Description |
|------------|--------|---------------------------------|
| currentTime | number | Current time after seeking |
| seekTime | number | Requested seek time |
| ----------- | ------ | ------------------------------- |
| currentTime | number | The current time after the seek |
| seekTime | number | The requested time |
Example:
**Example:**
```javascript
{
currentTime: 100.5,
seekTime: 100
currentTime: 100.5;
seekTime: 100;
}
```
> **Note:** On iOS, this callback is not reported when native controls are enabled.
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, when controls are enable, this callback is not reported. This is a known limitation.
### `onTimedMetadata`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
Triggered when timed metadata is available.
Callback function that is called when timed metadata becomes available
Payload:
**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']} />
Triggered when new subtitle data becomes available.
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:
**Payload:**
| Property | Type | Description |
|----------------|--------|--------------------------------------------------|
| subtitleTracks | string | The subtitle text content in a compatible format |
| ---------------- | -------- | -------------------------------------------------- |
| `subtitleTracks` | `string` | The subtitles text content in a compatible format. |
Example:
**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']} />
Triggered when available text (subtitle) tracks change.
Callback function that is called when text tracks change
Payload:
**Payload:**
| Property | Type | Description |
|----------|--------|--------------------------------------------------------------------------------------------------------------|
| -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| index | number | Internal track ID |
| 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 |
| 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:
**Example:**
```javascript
{
textTracks: [
{
index: 0,
title: 'English Subtitles',
type: 'vtt',
selected: true
}
]
title: 'Any Time You Like',
type: 'srt',
selected: true,
},
];
}
```
---
### `onVideoTracks`
<PlatformsList types={['Android']} />
Triggered when video tracks change.
Callback function that is called when video tracks change
Payload:
**Payload:**
| Property | Type | Description |
|----------|---------|-----------------------------------|
| index | number | Track index |
| -------- | ------- | --------------------------------------------------------------- |
| index | number | index of the track |
| trackId | string | Internal track ID |
| 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) |
| 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:
**Example:**
```javascript
{
videoTracks: [
{
index: 0,
trackId: "1",
codecs: "video/mp4",
index: O,
trackId: "0",
codecs: 'video/mp4',
width: 1920,
height: 1080,
bitrate: 5000000,
bitrate: 10000,
selected: true,
rotation: 0
}
]
rotation: 0,
},
];
}
```
---
### `onVolumeChange`
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
Triggered when the player volume changes.
Callback function that is called when the volume of player changes.
> **Note:** This event applies to the player's volume, not the device's system volume.
> Note: This event applies to the volume of the player, not the volume of the device.
Payload:
**Payload:**
| Property | Type | Description |
|----------|--------|---------------------------------|
| volume | number | Volume level (0 to 1) |
| -------- | ------ | ------------------------------------------ |
| volume | number | The volume of the player (between 0 and 1) |
Example:
**Example:**
```javascript
{
volume: 0.5
volume: 0.5;
}
```

View File

@@ -2,336 +2,234 @@ import PlatformsList from '../../components/PlatformsList/PlatformsList.tsx';
# Methods
This page shows the list of available methods.
## Details
This page shows the list of available methods
### `dismissFullscreenPlayer`
<PlatformsList types={['Android', 'iOS', 'web']} />
```tsx
dismissFullscreenPlayer(): Promise<void>
```
`dismissFullscreenPlayer(): Promise<void>`
Exits fullscreen mode.
Take the player out of fullscreen mode.
> **Deprecated:** Use `setFullScreen(false)` instead.
---
> [!WARNING]
> deprecated, use setFullScreen method instead
### `pause`
<PlatformsList types={['Android', 'iOS', 'web']} />
```tsx
pause(): Promise<void>
```
`pause(): Promise<void>`
Pauses the video.
---
Pause the video.
### `presentFullscreenPlayer`
<PlatformsList types={['Android', 'iOS', 'web']} />
```tsx
presentFullscreenPlayer(): Promise<void>
```
`presentFullscreenPlayer(): Promise<void>`
Enters fullscreen mode.
Put the player in fullscreen mode.
- 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 iOS, this displays the video in a fullscreen view controller with controls.
> **Deprecated:** Use `setFullScreen(true)` instead.
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.
---
> [!WARNING]
> deprecated, use setFullScreen method instead
### `resume`
<PlatformsList types={['Android', 'iOS', 'web']} />
```tsx
resume(): Promise<void>
```
`resume(): Promise<void>`
Resumes video playback.
---
Resume the video.
### `restoreUserInterfaceForPictureInPictureStopCompleted`
<PlatformsList types={['iOS']} />
```tsx
restoreUserInterfaceForPictureInPictureStopCompleted(restored)
```
`restoreUserInterfaceForPictureInPictureStopCompleted(restored)`
Must be called after `onRestoreUserInterfaceForPictureInPictureStop`.
Corresponds to Apple's [`restoreUserInterfaceForPictureInPictureStop`](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc).
---
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.
### `save`
<PlatformsList types={['iOS']} />
```tsx
save(): Promise<{ uri: string }>
```
`save(): Promise<{ uri: string }>`
Saves the video to the user's **Photos app** with the current filter.
Save video to your Photos with current filter prop. Returns promise.
#### 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**.
Notes:
#### Future improvements:
- Support for **multiple quality options**.
- Support for **more formats**.
- Support for **custom directory and filename**.
- 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:
### `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.
---
- Will support multiple qualities through options
- Will support more formats in the future through options
- Will support custom directory and file name through options
### `seek`
<PlatformsList types={['All']} />
```tsx
seek(seconds: number)
```
`seek(seconds)`
Seeks to the specified position (**in seconds**).
Seek to the specified position represented by seconds. seconds is a float value.
#### Notes:
- **Must be called after** `onLoad`.
- Triggers the [`onSeek`](./events#onseek) event.
`seek()` can only be called after the `onLoad` event has fired. Once completed, the [onSeek](./events#onseek) event will be called.
#### **iOS Exact Seek:**
```tsx
seek(seconds, tolerance: number)
```
- Default **tolerance**: ±100ms.
- Set `tolerance = 0` for **precise seeking**.
#### Exact seek
---
<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']} />
```tsx
setVolume(value: number): Promise<void>
```
`setVolume(value): Promise<void>`
Changes the **volume** level. Same behavior as the [`volume`](./props#volume) prop.
---
This function will change the volume exactly like [volume](./props#volume) property. default value and range are the same then.
### `getCurrentPosition`
<PlatformsList types={['Android', 'iOS', 'web']} />
```tsx
getCurrentPosition(): Promise<number>
```
`getCurrentPosition(): Promise<number>`
Returns the **current playback position** in seconds.
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.
> **Throws an error** if the player is not initialized.
---
### `setSource`
<PlatformsList types={['Android', 'iOS']} />
```tsx
setSource(source: ReactVideoSource): Promise<void>
```
`setSource(source: ReactVideoSource): Promise<void>`
Updates the media source **dynamically**.
> **Note:** This **overrides** the `source` prop.
---
This function will change the source exactly like [source](./props#source) property.
Changing source with this function will overide source provided as props.
### `setFullScreen`
<PlatformsList types={['Android', 'iOS', 'web']} />
```tsx
setFullScreen(fullscreen: boolean): Promise<void>
```
`setFullScreen(fullscreen): Promise<void>`
Toggles fullscreen mode.
If you set it to `true`, the player enters fullscreen mode. If you set it to `false`, the player exits fullscreen mode.
- `true` → Enters fullscreen.
- `false` → Exits fullscreen.
On iOS, this displays the video in a fullscreen view controller with controls.
---
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 **reference to the native HTML `<video>` element**.
Useful for integrating **third-party** video libraries like **hls.js, shaka, video.js, etc.**.
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...).
---
### **Example Usage**
### Example Usage
```tsx
const videoRef = useRef<VideoRef>(null);
const handleVideoControls = async () => {
if (!videoRef.current) return;
const someCoolFunctions = async () => {
if (!videoRef.current) {
return;
}
// Fullscreen controls
// present or dismiss fullscreen player
videoRef.current.presentFullscreenPlayer();
videoRef.current.dismissFullscreenPlayer();
// Playback controls
// pause or resume the video
videoRef.current.pause();
videoRef.current.resume();
// Save video
// save video to your Photos with current filter prop
const response = await videoRef.current.save();
console.log('Saved video path:', response.uri);
const path = response.uri;
// Seek to 200s (or with tolerance on iOS)
// seek to the specified position represented by seconds
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']} />
```tsx
getWidevineLevel(): Promise<number>
```
Indicates whether the widevine level supported by device.
Returns the **Widevine DRM level**:
Possible values are:
- **0** → Unknown / Not supported.
- **1, 2, 3** → Supported Widevine levels.
---
- 0 - unable to determine widevine support (typically not supported)
- 1, 2, 3 - Widevine level supported
### `isCodecSupported`
<PlatformsList types={['Android', 'web']} />
```tsx
isCodecSupported(mimetype: string, width: number, height: number): Promise<'hardware' | 'software' | 'unsupported'>
```
Indicates whether the provided codec is supported level supported by device.
Checks if the given **video codec** is supported.
parameters:
| Result | Meaning |
|------------|--------------------------------------|
| `hardware` | Hardware decoding supported |
| `software` | Only software decoding available |
| `unsupported` | Codec **not supported** |
- `mimetype`: mime type of codec to query
- `width`, `height`: resolution to query
---
Possible results:
- `hardware` - codec is supported by hardware
- `software` - codec is supported by software only
- `unsupported` - codec is not supported
### `isHEVCSupported`
<PlatformsList types={['Android']} />
```tsx
isHEVCSupported(): Promise<boolean>
```
Helper which Indicates whether the provided HEVC/1920\*1080 is supported level supported by device. It uses isCodecSupported internally.
Checks if **HEVC (H.265)** is supported at **1920×1080 resolution**.
> Uses `isCodecSupported` internally.
---
### Static Methods Example Usage
### 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,42 +1,43 @@
# 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 to render media content like videos and streams.
`react-native-video` is a React Native library that provides a Video component that renders media content such as videos and streams
It allows you to stream video files (m3u, mpd, mp4, etc.) inside your React Native application.
It allows to stream video files (m3u, mpd, mp4, ...) 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 (embedded or side-loaded)
- Subtitles (embeded or side loaded)
- DRM support
- Client-side ad insertion (via Google IMA)
- PiP (Picture-in-Picture)
- Client side Ads insertion (via google IMA)
- Pip (ios)
- Embedded playback controls
- And more
- And much more
The goal of this package is to provide lightweight but full control over the player.
The aim of this package is to have a thin and exhaustive control of player.
## V6.0.0 Information
> ⚠️ **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**: 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.x requires **react-native >= 0.68.2**
> ⚠️ From **6.0.0-beta.8**, it also requires **iOS >= 13.0** (default in React Native 0.73).
> ⚠️ from **6.0.0-beta.8** requires also **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';
// Inside your render function, assuming you have a file called
import Video, {VideoRef} from 'react-native-video';
// 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 needed.
// on a single screen if you like.
const VideoPlayer = () => {
const videoRef = useRef<VideoRef>(null);
@@ -50,14 +51,14 @@ const VideoPlayer = () => {
ref={videoRef}
// Callback when remote video is buffering
onBuffer={onBuffer}
// Callback when the video cannot be loaded
// Callback when video cannot be loaded
onError={onError}
style={styles.backgroundVideo}
/>
);
};
)
}
// Later in your styles...
// Later on in your styles..
var styles = StyleSheet.create({
backgroundVideo: {
position: 'absolute',
@@ -68,4 +69,3 @@ var styles = StyleSheet.create({
},
});
```

View File

@@ -1,5 +1,6 @@
# Installation
# Installation
Using npm:
```shell
@@ -12,99 +13,76 @@ or using yarn:
yarn add react-native-video
```
Then follow the instructions for your platform to link `react-native-video` into your project.
# Specific Platform Installation
Then follow the instructions for your platform to link react-native-video into your project
# 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 the [updating section](updating.md).
⚠️ from version `6.0.0` the minimum iOS version required is `13.0`. For more information see [updating section](updating.md)
### Enable Custom Features in the Podfile
### Enable custom feature in podfile file
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).
Samples available in sample app see [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, add the following line to your Podfile: ([more info here](other/caching.md))
To enable Video caching usage, add following line in 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 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:
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.
To enable google IMA usage define add following line in your podfile:
```podfile
$RNVideoUseGoogleIMA=true
```
**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.
**If you are using Expo you can use [expo plugin](other/expo.md) for it**
</details>
<details>
<summary>Android</summary>
## Android
From version `>= 6.0.0`, your application must use Kotlin version `>= 1.8.0`.
From version >= 6.0.0, your application needs to have 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 Features in the Gradle File
### Enable custom feature in gradle file
**If you are using Expo, you can use the [Expo plugin](other/expo.md).**
**If you are using Expo you can use [expo plugin](other/expo.md) for it**
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)
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)
- `useExoplayerRtsp` - Enable RTSP support
- `useExoplayerSmoothStreaming` - Enable SmoothStreaming support
- `useExoplayerDash` - Enable Dash support
- `useExoplayerHls` - Enable HLS support
Each enabled feature increases the APK size, so only enable what you need.
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`
By default, the enabled features are:
- `useExoplayerSmoothStreaming`
- `useExoplayerDash`
- `useExoplayerHls`
Example:
@@ -122,10 +100,9 @@ buildscript {
}
```
See the [sample app](https://github.com/TheWidlarzGroup/react-native-video/blob/9c669a2d8a53df36773fd82ff0917280d0659bc7/examples/basic/android/build.gradle#L14C5-L14C5).
See [sample app](https://github.com/TheWidlarzGroup/react-native-video/blob/9c669a2d8a53df36773fd82ff0917280d0659bc7/examples/basic/android/build.gradle#L14C5-L14C5)
</details>
<details>
<summary>Windows</summary>
@@ -135,99 +112,81 @@ See the [sample app](https://github.com/TheWidlarzGroup/react-native-video/blob/
**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 manual additions:
Make the following additions to the given files manually:
#### `windows\myapp.sln`
`windows\myapp.sln`
Add the _ReactNativeVideoCPP_ project to your solution:
Add the _ReactNativeVideoCPP_ project to your solution (eg. `windows\myapp.sln`):
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`.
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`
#### `windows\myapp\myapp.vcxproj`
`windows\myapp\myapp.vcxproj`
Add a reference to _ReactNativeVideoCPP_ to your main application project:
Add a reference to _ReactNativeVideoCPP_ to your main application project (eg. `windows\myapp\myapp.vcxproj`):
1. Open your solution in Visual Studio 2019.
2. Right-click the main application project > Add > Reference...
3. Check _ReactNativeVideoCPP_ from Solution Projects.
1. Open your solution in Visual Studio 2019
2. Right-click main application project > Add > Reference...
3. Check _ReactNativeVideoCPP_ from Solution Projects
#### `pch.h`
`pch.h`
Add:
Add `#include "winrt/ReactNativeVideoCPP.h"`.
```cpp
#include "winrt/ReactNativeVideoCPP.h"
```
`app.cpp`
#### `app.cpp`
Add:
```cpp
PackageProviders().Append(winrt::ReactNativeVideoCPP::ReactPackageProvider());
```
before `InitializeComponent();`.
Add `PackageProviders().Append(winrt::ReactNativeVideoCPP::ReactPackageProvider());` before `InitializeComponent();`.
**React Native Windows 0.61 and below**
Follow the manual linking steps for React Native Windows 0.62, but use _ReactNativeVideoCPP61_ instead of _ReactNativeVideoCPP_.
Follow the manual linking instructions for React Native Windows 0.62 above, but substitute _ReactNativeVideoCPP61_ for _ReactNativeVideoCPP_.
</details>
<details>
<summary>tvOS</summary>
## tvOS
`react-native link react-native-video` does not work properly with the tvOS target, so the library must be added manually.
`react-native link react-native-video` doesnt work properly with the tvOS target so we need to add the library manually.
### Steps:
First select your project in Xcode.
1. Select your project in Xcode.
![tvOS step 1](../assets/tvOS-step-1.jpg)
![tvOS step 1](../assets/tvOS-step-1.jpg)
After that, select the tvOS target of your application and select « General » tab
2. Select the tvOS target of your application and open the "General" tab.
![tvOS step 2](../assets/tvOS-step-2.jpg)
![tvOS step 2](../assets/tvOS-step-2.jpg)
Scroll to « Linked Frameworks and Libraries » and tap on the + button
3. Scroll to "Linked Frameworks and Libraries" and click the `+` button.
![tvOS step 3](../assets/tvOS-step-3.jpg)
![tvOS step 3](../assets/tvOS-step-3.jpg)
4. Select `RCTVideo-tvOS`.
![tvOS step 4](../assets/tvOS-step-4.jpg)
Select RCTVideo-tvOS
![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>
## Web
Nothing to do, everything should work out of the box.
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.
Note that only basic video support is present, no hls/dash or ads/drm for now.
</details>

View File

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

View File

@@ -1,14 +1,12 @@
# 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 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.
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.
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.
In addition, this resolves RNV6's repeated source URI call problem when looping a video on Android.
## iOS
@@ -16,33 +14,17 @@ Additionally, this resolves the issue in RNV6 where the source URI was repeatedl
The cache is backed by [SPTPersistentCache](https://github.com/spotify/SPTPersistentCache) and [DVAssetLoaderDelegate](https://github.com/vdugnist/DVAssetLoaderDelegate).
### How It Works
### How Does It Work
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.
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.
### Restrictions
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.
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.
At this time, HLS playlists (`.m3u8`) and videos with sideloaded text tracks are not supported and will bypass the cache.
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!
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.
By default files expire after 30 days and the maximum cache size is 100mb.
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.
In a future release the cache might have more configurable options.

View File

@@ -1,38 +1,20 @@
# Debugging
This page provides useful tips for debugging and troubleshooting issues in the package or your application.
This page describe useful tips for debugging and investigating issue in the package or in your application.
## Using the Sample App
## 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```
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.
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):
## 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)
```xml
<application
@@ -43,56 +25,56 @@ If your video works in Debug mode but shows only a black screen in Release mode,
## Decoder Issue (Android)
Some devices have a maximum number of simultaneous video playbacks. If this limit is reached, ExoPlayer returns an error: `Unable to instantiate decoder`.
Devices have a maximum of simultaneous possible playback. It means you have reach this limit. Exoplayer returns: 'Unable to instantiate decoder'
**Known issue:** This happens frequently in Debug mode.
**known issue**: This issue happen really often in debug mode.
## Unable to Play Clear Content (All OS)
## You cannot play clean content (all OS)
Before opening a ticket, follow these steps:
Here are the steps to consider before opening a ticket in issue tracker
### Check Remote File Access
## Check you can access to remote file
Ensure you can download the manifest/content file using a browser.
Ensure you can download to manifest / content file with a browser for example
### Check If Another Player Can Play the Content
## Check another player can read the content
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.
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)
## Unable to Play Protected Content (All OS)
## You cannot play protected content (all OS)
### Protected Content Gives an Error (Token Error / Access Forbidden)
## Protected content gives error (token error / access forbidden)
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.
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.
## Debugging Network Calls Not Visible in React Native Debugging Tools
## I need to debug network calls but I don't see them in react native debugging tools
This is a React Native limitationReact Native debugging tools only capture network calls made in JavaScript.
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, ...
To debug network calls, use tools like:
- [Charles Proxy](https://www.charlesproxy.com/)
- [Fiddler](https://www.telerik.com/fiddler)
Then try to compare exchanges with previous tests you made.
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.
## Debug media3: build from media3 source
## Debugging Media3: Build from Media3 Source
If you need to use a specific exoplayer version or patch default behavior, you may want to build from media3 source code.
If you need to use a specific ExoPlayer version or modify default behavior, you may need to build from the Media3 source code.
Building from media3 source is possible. You need to add 2 or 3 things in your app:
### Configure Player Path
### Configure player path
Add the following lines to `settings.gradle` to configure your Media3 source path:
You need to add following lines in settings.gradle to configure your media3 source path:
```gradle
gradle.ext.androidxMediaModulePrefix = 'media-'
apply from: file("../../../../media3/core_settings.gradle")
```
````
Replace this with the actual Media3 source path. Ensure that you use the same version (or a compatible API version) supported by the package.
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.
### Enable Building from Source
In your `build.gradle` file, add the following setting:
### Enable building from source
In your build.gradle file, add following setting:
```gradle
buildscript {
@@ -104,7 +86,6 @@ buildscript {
}
```
## 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).
## It's still not working
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

@@ -1,62 +0,0 @@
# 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
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
## 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.
```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,4 +38,3 @@ This is particularly useful when using the `Expo` managed workflow (`expo prebui
| 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,60 +2,54 @@
## iOS App Transport Security
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:
- 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:
![App Transport Security](../../assets/AppTransportSecuritySetting.png)
For more details, check this [article](https://cocoacasts.com/how-to-add-app-transport-security-exception-domains).
For more detailed info check this [article](https://cocoacasts.com/how-to-add-app-transport-security-exception-domains)
</details>
## Audio Mixing
In future versions, `react-native-video` will include an Audio Manager for configuring how videos mix with other audio-playing apps.
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.
On iOS, if you want to allow background music from other apps to continue playing over your video component, update your `AppDelegate.m` file:
On iOS, if you would like to allow other apps to play music over your video component, make the following change:
### **AppDelegate.m**
**AppDelegate.m**
```objective-c
#import <AVFoundation/AVFoundation.h> // Import the AVFoundation framework
#import <AVFoundation/AVFoundation.h> // import
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil]; // Allow background audio
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil]; // allow
...
}
```
You can also use the [`ignoreSilentSwitch`](#ignoresilentswitch) prop.
You can also use the [ignoreSilentSwitch](#ignoresilentswitch) prop.
</details>
## 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.
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:
This only supports mp4 files and they must not be compressed. Example command line for preventing compression:
```bash
zip -r -n .mp4 *.mp4 player.video.example.com
```
### Example Usage in Code:
```javascript
// 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}
/>
// 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} />
```
## Load Files with the React Native Asset System
## Load files with the RN Asset System
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:
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`.
```javascript
<Video
@@ -63,8 +57,6 @@ The asset system introduced in RN `0.14` allows loading shared image resources a
/>
```
## Play in Background on iOS
## Play in background on iOS
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).)_
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) )

View File

@@ -1,18 +1,12 @@
# New Architecture
## Fabric
The library currently does not support Fabric. We are working on adding support. In the meantime, you can use the Interop Layer.
Library currently does not support Fabric. We are working on it. In the meantime, you can use 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`.
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:
For `react-native` < `0.74` you need to add config in `react-native.config.js` file.
```javascript
module.exports = {
@@ -28,5 +22,4 @@ module.exports = {
```
## Bridgeless Mode
The library currently does not support Bridgeless Mode. We are working on adding support.
Library currently does not support Bridgeless Mode. We are working on it.

View File

@@ -1,312 +1,115 @@
# Plugin
# Plugin (experimental)
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).
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)
## Commercial Plugins
## Concept
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.
Most of the analytics system which tracks player information (bitrate, errors, ...) can be integrated directly with Exoplayer or AVPlayer handles.
> Using or recommending our commercial software helps support the maintenance of this open-source project. Thank you!
This plugin system allows none intrusive integration of analytics in the react-native-package. It shall be done in native language (kotlin/swift).
## Plugins for Analytics
The idea behind this system is to be able to plug an analytics package to react native video without doing any code change (ideally).
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.
Following documentation will show on how to create a new plugin for react native video
The goal is to enable easy analytics integration without modifying `react-native-video` itself.
## 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.
## Warnings & Considerations
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.
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.
## General
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
First you need to create a new react native package:
````shell
npx create-react-native-library@latest react-native-video-custom-analytics
```
````
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`.
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.
## Plugin Types
## Android
There is no special requierement for gradle file.
You need two mandatory action to be able to receive player handle
There are two types of plugins you can implement:
### 1/ Create the plugin
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
First you should instanciate a class which extends `RNVPlugin`.
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 proposed integration implement `RNVPlugin` directly inside the Module file (`VideoPluginSampleModule`).
## Android Implementation
### 1. Create the Plugin
You can implement either the base `RNVPlugin` interface or the player-specific `RNVExoplayerPlugin` interface.
#### Base Plugin
The `RNVPlugin` interface only defines 2 functions, see description here under.
```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 the plugin within the main `react-native-video` package, call:
To register this allocated class in the main react native video package you should call following function:
```kotlin
ReactNativeVideoManager.getInstance().registerPlugin(plugin)
```
The proposed integration register the instanciated class in `createNativeModules` entry point.
In the sample implementation, the plugin is registered in the `createNativeModules` entry point.
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.
## ios
### Extending Core Functionality via Plugins
### 1/ podspec integration
In addition to analytics, plugins can also be used to modify or override core behavior of `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.
These capabilities are available through the advanced Android plugin interface: `RNVExoplayerPlugin`.
> ⚠️ These extension points are optional — if no plugin provides them, the player behaves exactly as it did before.
---
#### Plugin Extension Points (Android)
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
}
```
##### 2. `overrideMediaDataSourceFactory`
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:
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.
```podfile
s.dependency "react-native-video"
```
s.dependency "react-native-video"
````
### 2. Create the Plugin
### 2/ Create the plugin
You can implement either the base `RNVPlugin` class or the player-specific `RNVAVPlayerPlugin` class.
First you should instanciate a class which extends `RNVPlugin`.
#### Base Plugin
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.
```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
/**
* Function called when a new player is created
* @param player: the instantiated player reference
*/
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
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
*/
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 }
func onInstanceRemoved(player: Any)
```
##### `OverridePlayerAssetResult` and `OverridePlayerAssetType`
### 3/ Register the plugin
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:
To register this allocated class in the main react native video package you should register it by calling this function:
```swift
ReactNativeVideoManager.shared.registerPlugin(plugin: plugin)
```
In the sample implementation, the plugin is registered inside the `VideoPluginSample` file within the `init` function:
The proposed integration register the instanciated class in file `VideoPluginSample` in the init function:
```swift
import react_native_video
@@ -319,118 +122,4 @@ override init() {
}
```
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
Your native module can now track Player updates directly from Player reference and report to backend.

View File

@@ -1,19 +1,11 @@
# Useful Projects
# 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.
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.
## 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
## 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.
## Other tools
- [react-native-track-player](https://github.com/doublesymmetry/react-native-track-player): A toolbox to control player over media session

View File

@@ -1,14 +1,13 @@
# Updating
## Version 6.0.0
### Version 6.0.0
### iOS
#### iOS
#### 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:
##### 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.
You can do it by adding the following code to your Podfile:
```diff
- platform :ios, min_ios_version_supported
@@ -18,16 +17,16 @@ You can do this by adding the following code to your Podfile:
+ end
```
#### 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.
##### 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.
Add `use_frameworks! :linkage => :static` right below `platform :ios` in your iOS project Podfile.
Add `use_frameworks! :linkage => :static` just under `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 the following lines from your Podfile as they are no longer needed:
You can remove following lines from your podfile as they are not necessary anymore
```diff
- `pod 'react-native-video', :path => '../node_modules/react-native-video/react-native-video.podspec'`
@@ -35,35 +34,19 @@ You can remove the following lines from your Podfile as they are no longer neede
- `pod 'react-native-video/VideoCaching', :path => '../node_modules/react-native-video/react-native-video.podspec'`
```
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.
If you were previously using VideoCaching, you should $RNVideoUseVideoCaching flag in your podspec, see: [installation section](https://docs.thewidlarzgroup.com/react-native-video/installation#video-caching)
> **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.
#### Android
### Android
If you were using ExoPlayer on V5, remove the patch from **android/settings.gradle**:
If you are already using Exoplayer on V5, you should remove the patch done 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.
@@ -83,5 +66,4 @@ allprojects {
}
}
```
If you encounter the error `Could not find com.android.support:support-annotations:27.0.0.`, reinstall your Android Support Repository.
If you encounter an error `Could not find com.android.support:support-annotations:27.0.0.` reinstall your Android Support Repository.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,6 +1,4 @@
import React from 'react';
import TWGBadge from './components/TWGBadge/TWGBadge';
import jsonLd from './json-ld.json';
export default {
head: (
@@ -28,27 +26,27 @@ export default {
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
rel="icon"
type="image/x-icon"
href="https://docs.thewidlarzgroup.com/react-native-video/favicon.ico"
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400..900&display=swap"
rel="stylesheet"
/>
<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"
href="https://docs.thewidlarzgroup.com/react-native-video/favicon.png"
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
async
src="https://www.googletagmanager.com/gtag/js?id=G-PM2TQQQMDN"
/>
<script>
{`window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-PM2TQQQMDN');`}
</script>
</>
),
logo: (
@@ -61,23 +59,68 @@ export default {
},
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>TheWidlarzGroup</strong> &{' '}
<strong>React Native Community</strong>
Built with by <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

@@ -8,6 +8,12 @@ This directory contains examples for `react-native-video` - this is a guide that
- **[`expo`](#expo)** - Expo example that you can run on: iOS, Android, tvOS, web
### Updating Examples Content
Both of applications have mostly the same code (Windows and tvOS have platform-specific code). Other platform are using codebase from `bare` example.
If you want to update examples content, you should do it in `bare` example. `expo` example is copping (and overwriting!) `src` folder from `bare` on dependency install.
If you want to sync `expo` example, you can use `yarn update-src` command in `expo` example directory.
## How To Run Examples
## [Bare](https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples/bare)
@@ -17,7 +23,7 @@ This directory contains examples for `react-native-video` - this is a guide that
You can configure the example by changing the settings of expo-plugin `app.json` file in the `bare` directory.
> [!TIP]
> You can find more information about the expo-plugin configuration [here](https://docs.thewidlarzgroup.com/react-native-video/other/expo/?utm_source=rnv&utm_medium=readme&utm_campaign=example&utm_id=expo-plugins-bare).
> You can find more information about the expo-plugin configuration [here](https://docs.thewidlarzgroup.com/react-native-video/other/expo).
> [!CAUTION]
> You will need to regenerate the native project after changing the `app.json` file. eg. on Apple platforms you will need to install pods twice. (one for applying expo-plugin changes and second for applying react-native-video changes)
@@ -83,10 +89,6 @@ yarn start
## [Expo](https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples/expo)
> [!NOTE]
> Additionally, there is a great example of a TV app available in the [AmazonAppDev/react-native-multi-tv-app-sample](https://github.com/AmazonAppDev/react-native-multi-tv-app-sample) repository.
It provides a sample application for Android TV, Fire TV, tvOS, and the web. The app includes customizable drawer navigation, a content grid, a hero header, and an integrated video player. Built with Expo, it serves as a great starting point for cross-platform TV app development.
### Configuration
#### Expo Plugin
@@ -94,7 +96,7 @@ It provides a sample application for Android TV, Fire TV, tvOS, and the web. The
You can configure the example by changing the settings of expo-plugin `app.json` file in the `expo` directory.
> [!TIP]
> You can find more information about the expo-plugin configuration [here](https://docs.thewidlarzgroup.com/react-native-video/other/expo/?utm_source=rnv&utm_medium=readme&utm_campaign=example&utm_id=expo-plugins).
> You can find more information about the expo-plugin configuration [here](https://docs.thewidlarzgroup.com/react-native-video/other/expo).
> [!CAUTION]
> You will need to regenerate the native project after changing the `app.json` file - you can do it by running `yarn prebuild` command in `expo` example directory.

View File

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

View File

@@ -9,7 +9,6 @@
.xcode.env
Pods/
build/
vendor/bundle/
dist/*
!dist/.gitignore
local.properties

View File

@@ -1,9 +0,0 @@
source 'https://rubygems.org'
ruby ">= 2.6.10"
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
gem 'xcodeproj', '< 1.26.0'
gem 'concurrent-ruby', '< 1.3.4'
gem 'cocoapods-swift-modular-headers'

View File

@@ -1,111 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
activesupport (6.1.7.10)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
atomos (0.1.3)
base64 (0.2.0)
claide (1.1.0)
cocoapods (1.15.2)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.15.2)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 2.1, < 3.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.6.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0)
xcodeproj (>= 1.23.0, < 2.0)
cocoapods-core (1.15.2)
activesupport (>= 5.0, < 8)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (2.1)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
cocoapods-swift-modular-headers (0.0.2)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored2 (3.1.2)
concurrent-ruby (1.3.3)
escape (0.0.4)
ethon (0.16.0)
ffi (>= 1.15.0)
ffi (1.17.2)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
httpclient (2.9.0)
mutex_m
i18n (1.14.7)
concurrent-ruby (~> 1.0)
json (2.7.6)
minitest (5.25.4)
molinillo (0.8.0)
mutex_m (0.3.0)
nanaimo (0.3.0)
nap (1.1.0)
netrc (0.11.0)
nkf (0.2.0)
public_suffix (4.0.7)
rexml (3.4.1)
ruby-macho (2.5.1)
typhoeus (1.4.1)
ethon (>= 0.9.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
xcodeproj (1.25.1)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (>= 3.3.6, < 4.0)
zeitwerk (2.6.18)
PLATFORMS
ruby
DEPENDENCIES
activesupport (>= 6.1.7.5, != 7.1.0)
cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
cocoapods-swift-modular-headers
concurrent-ruby (< 1.3.4)
xcodeproj (< 1.26.0)
RUBY VERSION
ruby 2.6.10p210
BUNDLED WITH
1.17.2

View File

@@ -18,7 +18,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryEr
# section on Gradle build performance:
# https://docs.gradle.org/current/userguide/performance.html#parallel_execution.
# Default is `false`.
# org.gradle.parallel=true
#org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
@@ -40,19 +40,14 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
# Note that this is incompatible with web debugging.
# newArchEnabled=true
# bridgelessEnabled=true
#newArchEnabled=true
#bridgelessEnabled=true
# Uncomment the line below to build React Native from source.
# react.buildFromSource=true
#react.buildFromSource=true
# Version of Android NDK to build against.
# ANDROID_NDK_VERSION=26.1.10909125
#ANDROID_NDK_VERSION=26.1.10909125
# Version of Kotlin to build against.
# KOTLIN_VERSION=1.8.22
RNVideo_useExoplayerRtsp=true
RNVideo_useExoplayerSmoothStreaming=true
RNVideo_useExoplayerDash=true
RNVideo_useExoplayerHls=true
#KOTLIN_VERSION=1.8.22

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -86,8 +84,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

View File

@@ -13,8 +13,6 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################

View File

@@ -3,9 +3,9 @@
*/
import {AppRegistry} from 'react-native';
import BasicExample from 'common/BasicExample';
import BasicExample from './src/BasicExample';
import {name as appName} from './app.json';
import DRMExample from 'common/DRMExample';
import DRMExample from './src/DRMExample';
AppRegistry.registerComponent(appName, () => BasicExample);
AppRegistry.registerComponent('DRMExample', () => DRMExample);

View File

@@ -4,28 +4,20 @@ ws_dir = ws_dir.parent until
ws_dir.expand_path.to_s == '/'
require "#{ws_dir}/node_modules/react-native-test-app/test_app.rb"
if ENV['RCT_NEW_ARCH_ENABLED'].nil?
ENV['RCT_NEW_ARCH_ENABLED'] = '0'
end
workspace 'BareExample.xcworkspace'
if ENV['RCT_NEW_ARCH_ENABLED'] == '1'
Pod::UI.puts "New Architecture is ENABLED".green
else
Pod::UI.puts "New Architecture is DISABLED".red
end
use_test_app!
# This is used by CI to test different configurations
# If you want to enable it look to README.md
if ENV['RNV_SAMPLE_ENABLE_ADS']
$RNVideoUseGoogleIMA = true
end
if ENV['RNV_SAMPLE_VIDEO_CACHING']
plugin 'cocoapods-swift-modular-headers'
$RNVideoUseVideoCaching = true
apply_modular_headers_for_swift_dependencies()
end
workspace 'BareExample.xcworkspace'
use_test_app!
# Chache dependencies need to have modular headers
if defined?($RNVideoUseVideoCaching)
use_modular_headers!
end

File diff suppressed because it is too large Load Diff

View File

@@ -12,16 +12,9 @@ module.exports = makeMetroConfig({
},
resolver: {
enableSymlinks: true,
// Add support for ../common by including it in extraNodeModules
extraNodeModules: {
common: path.resolve(__dirname, '../common'),
'react-native-video': path.resolve(__dirname, '../../lib/'),
'@react-native-picker/picker': path.resolve(__dirname, 'node_modules/@react-native-picker/picker'),
},
},
watchFolders: [
path.join(__dirname, 'node_modules', 'react-native-video'),
path.resolve(__dirname, '../..'),
path.resolve(__dirname, '../common'),
],
});

View File

@@ -18,32 +18,32 @@
"windows": "react-native run-windows --sln windows/BareExample.sln"
},
"dependencies": {
"@callstack/react-native-visionos": "^0.78.0",
"@react-native-picker/picker": "2.11.0",
"react": "19.0.0",
"react-native": "^0.78.0",
"@callstack/react-native-visionos": "^0.73.0",
"@react-native-picker/picker": "2.8.1",
"react": "18.2.0",
"react-native": "0.73.2",
"react-native-video": "link:../..",
"react-native-windows": "^0.78.0"
"react-native-windows": "^0.73.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@expo/config-plugins": "^8.0.10",
"@react-native/babel-preset": "^0.78.0",
"@react-native/babel-preset": "0.73.19",
"@react-native/eslint-config": "0.73.2",
"@react-native/metro-config": "^0.78.0",
"@react-native/metro-config": "0.73.3",
"@react-native/typescript-config": "0.73.1",
"@rnx-kit/metro-config": "^2.0.0",
"@types/react": "^19.0.0",
"@types/react-test-renderer": "^19.0.0",
"@types/react": "^18.2.6",
"@types/react-test-renderer": "^18.0.0",
"babel-jest": "^29.6.3",
"eslint": "^8.19.0",
"jest": "^29.2.1",
"jest": "^29.6.3",
"patch-package": "^8.0.0",
"prettier": "2.8.8",
"react-native-test-app": "^4.1.4",
"react-test-renderer": "19.0.0",
"react-native-test-app": "^3.10.14",
"react-test-renderer": "18.2.0",
"typescript": "5.0.4"
},
"engines": {

View File

@@ -75,10 +75,6 @@ const BasicExample = () => {
useState(false);
const [isSeeking, setIsSeeking] = useState(false);
// Add refs to store previous track data for comparison
const previousAudioTracksRef = useRef<AudioTrack[]>([]);
const previousTextTracksRef = useRef<TextTrack[]>([]);
const videoRef = useRef<VideoRef>(null);
const viewStyle = fullscreen ? styles.fullScreen : styles.halfScreen;
const currentSrc = srcList[srcListId];
@@ -111,36 +107,7 @@ const BasicExample = () => {
const onAudioTracks = (data: OnAudioTracksData) => {
console.log('onAudioTracks', data);
// Check if audio tracks have actually changed
const currentTracks = data.audioTracks || [];
const previousTracks = previousAudioTracksRef.current;
// Simple comparison - check if tracks array length or selected track changed
const tracksChanged =
currentTracks.length !== previousTracks.length ||
JSON.stringify(
currentTracks.map((t) => ({
index: t.index,
selected: t.selected,
language: t.language,
})),
) !==
JSON.stringify(
previousTracks.map((t) => ({
index: t.index,
selected: t.selected,
language: t.language,
})),
);
if (!tracksChanged) {
return; // Skip if tracks haven't changed
}
previousAudioTracksRef.current = currentTracks;
const selectedTrack = currentTracks.find((x: AudioTrack) => {
const selectedTrack = data.audioTracks?.find((x: AudioTrack) => {
return x.selected;
});
let value;
@@ -151,7 +118,7 @@ const BasicExample = () => {
} else if (audioTracksSelectionBy === SelectedTrackType.TITLE) {
value = selectedTrack?.title;
}
setAudioTracks(currentTracks);
setAudioTracks(data.audioTracks);
setSelectedAudioTrack({
type: audioTracksSelectionBy,
value: value,
@@ -164,39 +131,11 @@ const BasicExample = () => {
};
const onTextTracks = (data: OnTextTracksData) => {
// Check if text tracks have actually changed
const currentTracks = data.textTracks || [];
const previousTracks = previousTextTracksRef.current;
// Simple comparison - check if tracks array length or selected track changed
const tracksChanged =
currentTracks.length !== previousTracks.length ||
JSON.stringify(
currentTracks.map((t) => ({
index: t.index,
selected: t.selected,
language: t.language,
})),
) !==
JSON.stringify(
previousTracks.map((t) => ({
index: t.index,
selected: t.selected,
language: t.language,
})),
);
if (!tracksChanged) {
return; // Skip if tracks haven't changed
}
previousTextTracksRef.current = currentTracks;
const selectedTrack = currentTracks.find((x: TextTrack) => {
const selectedTrack = data.textTracks?.find((x: TextTrack) => {
return x?.selected;
});
setTextTracks(currentTracks);
setTextTracks(data.textTracks);
let value;
if (textTracksSelectionBy === SelectedTrackType.INDEX) {
value = selectedTrack?.index;
@@ -282,6 +221,11 @@ const BasicExample = () => {
console.log('onVideoBandwidthUpdate', data);
};
const onFullScreenExit = () => {
// iOS pauses video on exit from full screen
Platform.OS === 'ios' && setPaused(true);
};
const _renderLoader = showPoster ? () => <VideoLoader /> : undefined;
const _subtitleStyle = {subtitlesFollowVideo: true};
@@ -296,7 +240,7 @@ const BasicExample = () => {
};
useEffect(() => {
videoRef.current?.setSource({...currentSrc, bufferConfig: _bufferConfig});
videoRef.current?.setSource({...currentSrc, bufferConfig: _bufferConfig });
}, [currentSrc]);
return (
@@ -317,6 +261,7 @@ const BasicExample = () => {
muted={muted}
controls={controls}
resizeMode={resizeMode}
onFullscreenPlayerWillDismiss={onFullScreenExit}
onLoad={onLoad}
onAudioTracks={onAudioTracks}
onTextTracks={onTextTracks}

View File

@@ -37,7 +37,7 @@ const DRMExample = () => {
// ------------- DMR Token -------------
// This token is used to authenticate the user and get the license
// To run example please go to https://www.thewidlarzgroup.com/services/free-drm-token-generator-for-video?utm_source=rnv&utm_medium=code&utm_campaign=drm&utm_id=commented and complete the form to receive the token
// To run example please go to https://www.thewidlarzgroup.com/services/free-drm-token-generator-for-video?utm_source=drm&utm_medium=code and complete the form to receive the token
// After you receive the token, please paste it here
const [token, setToken] = React.useState('<USER_TOKEN>');
@@ -97,9 +97,6 @@ const DRMExample = () => {
newSource.drm = {
type: DRMType.WIDEVINE,
licenseServer: widevineLicense,
headers: {
'x-drm-userToken': token,
},
};
newSource.uri = dash;
} else {

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