Compare commits
166 Commits
feat/shaka
...
codex/sync
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f513d4b2b | |||
| 5a1be8ba4c | |||
| d7563cfd93 | |||
| 69a9159a43 | |||
|
|
062fcec7fe | ||
|
|
ae7ff4fa07 | ||
|
|
cd72fc1f28 | ||
|
|
11962d9e02 | ||
|
|
92b0a0e416 | ||
|
|
fc936c49ef | ||
| c6bd3e3ee4 | |||
|
|
9c981f6f72 | ||
|
|
88ac1ae1dc | ||
| 2d953c4c46 | |||
|
|
f387177785 | ||
|
|
9d3ca8fd55 | ||
|
|
ba65ab1233 | ||
|
|
fbb260e916 | ||
|
|
90ccf4be29 | ||
|
|
f2afd16d0b | ||
|
|
05cd5972c2 | ||
|
|
a9f752435f | ||
|
|
5d75b48295 | ||
|
|
b561eff5a5 | ||
|
|
4a2229639c | ||
|
|
92514e72a6 | ||
|
|
5074ca5a1b | ||
|
|
7d233f414f | ||
|
|
724f63930d | ||
|
|
4b996fc514 | ||
|
|
d2c92a1f3f | ||
|
|
5b59b06d1e | ||
|
|
b11d59ba26 | ||
|
|
60baecdf73 | ||
|
|
b69ede1a27 | ||
|
|
31915e432b | ||
|
|
bf810386b9 | ||
|
|
e77a12ebaa | ||
|
|
481aa7ca65 | ||
|
|
86ea0e3101 | ||
|
|
2d7e03942d | ||
|
|
978683b645 | ||
|
|
1a4e3b0637 | ||
|
|
18527fcc5c | ||
|
|
94967fc4a6 | ||
|
|
b56b647d85 | ||
|
|
b51b579ff0 | ||
|
|
60c3a08ae3 | ||
|
|
79d25bf1bd | ||
|
|
3576a134e6 | ||
|
|
88c20d1c06 | ||
|
|
45fd250adf | ||
|
|
9f03cc5a09 | ||
|
|
9cf780276a | ||
|
|
55449d9ec4 | ||
|
|
9449eb34f3 | ||
|
|
e165cdefa1 | ||
|
|
6c5bb77def | ||
|
|
701f777b6e | ||
|
|
089e938aeb | ||
|
|
fc1e3f4fd1 | ||
|
|
ecfe12aa81 | ||
|
|
49173edd59 | ||
|
|
987be4b293 | ||
|
|
1c95041a4a | ||
|
|
368830954e | ||
|
|
dc4195e339 | ||
|
|
735e35a2ea | ||
|
|
b1b3db3010 | ||
|
|
2bdc0daf2e | ||
|
|
b17c319c32 | ||
|
|
40340467d7 | ||
|
|
ab8ec0699b | ||
|
|
8836362609 | ||
|
|
a849cc19e8 | ||
|
|
41ddc5c27a | ||
|
|
d2e5d9c64e | ||
|
|
697afd52f6 | ||
|
|
f188a7fd48 | ||
|
|
229a5764ea | ||
|
|
4458627051 | ||
|
|
ede502bf74 | ||
|
|
044e08a78d | ||
|
|
d902c1bf43 | ||
|
|
9f02614f5d | ||
|
|
fa20223c44 | ||
|
|
6e6f91517c | ||
|
|
b5103743e8 | ||
|
|
f9d3878ecc | ||
|
|
b11c1f315d | ||
|
|
bc533e53b0 | ||
|
|
9055b14cf4 | ||
|
|
82f5f3d21c | ||
|
|
13a5da8955 | ||
|
|
235c281219 | ||
|
|
057c287f12 | ||
|
|
b8459ca71d | ||
|
|
3edf511f9b | ||
|
|
6697cbf5d0 | ||
|
|
04eec42f1e | ||
|
|
451bafebd7 | ||
|
|
a8ca97f05f | ||
|
|
c8b800a508 | ||
|
|
faac5ad456 | ||
|
|
2905b61a0d | ||
|
|
74b1d5b540 | ||
|
|
ad52668d05 | ||
|
|
638f454a21 | ||
|
|
43955037bb | ||
|
|
3d3eba97e8 | ||
|
|
d90bf47df5 | ||
|
|
d9f92b800e | ||
|
|
8b952e709a | ||
|
|
8dc10fd4b7 | ||
|
|
449dfb62b5 | ||
|
|
6c3af99979 | ||
|
|
6cc1bff167 | ||
|
|
89ee02bdab | ||
|
|
3924b5e295 | ||
|
|
eff8ea24af | ||
|
|
c47d165668 | ||
|
|
424f4eedde | ||
|
|
d31c72fc04 | ||
|
|
69a7bc2d26 | ||
|
|
a735a4a581 | ||
|
|
78770d92f5 | ||
|
|
abc4d76099 | ||
|
|
16fa20411f | ||
|
|
3da4f1ca97 | ||
|
|
7b4bd9a016 | ||
| 7a6afd52a3 | |||
|
|
1033c9d4f3 | ||
|
|
d757a44bb1 | ||
|
|
64c222df44 | ||
|
|
621a80299c | ||
|
|
63c592f7cd | ||
|
|
569a79c510 | ||
|
|
f37dc9e33e | ||
|
|
dd78241b0d | ||
|
|
2b7c215e66 | ||
|
|
daaac9740a | ||
|
|
d934f214f5 | ||
|
|
d277c5e946 | ||
|
|
de81829d73 | ||
|
|
681aed0aed | ||
|
|
5fa77c4562 | ||
|
|
d45300270e | ||
|
|
0288d61e46 | ||
|
|
f78a407434 | ||
|
|
6080e96e82 | ||
|
|
5b3add4e7a | ||
|
|
f850b7a71c | ||
|
|
3b4bfd3936 | ||
|
|
21e2da3bfb | ||
|
|
5d532f5982 | ||
|
|
324fc2ac65 | ||
|
|
9eb5502076 | ||
| d7977241c9 | |||
| 921ead0f05 | |||
| 20397d32e6 | |||
| e3900e794d | |||
| 4dc7bf465f | |||
| e5f182cda9 | |||
|
|
7c7d83b6e5 | ||
|
|
7501880062 | ||
|
|
f04b233a40 |
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: TheWidlarzGroup
|
||||||
2
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -54,6 +54,6 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
If this functionality is important to you and you need it, contact [TheWidlarzGroup](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=feature-request#Contact) - [`hi@thewidlarzgroup.com`](mailto:hi@thewidlarzgroup.com)
|
If this functionality is important to you and you need it, contact [TheWidlarzGroup](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=feature-request&utm_campaign=issue-template&utm_id=sponsorship#Contact) - [`hi@thewidlarzgroup.com`](mailto:hi@thewidlarzgroup.com)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
.github/actions/setup-bun/action.yml
vendored
@@ -17,7 +17,7 @@ runs:
|
|||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
id: bun-cache
|
id: bun-cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
**/node_modules
|
**/node_modules
|
||||||
|
|||||||
4
.github/actions/setup-node/action.yml
vendored
@@ -13,11 +13,11 @@ runs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
id: yarn-cache
|
id: yarn-cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ inputs.working-directory }}/node_modules
|
${{ inputs.working-directory }}/node_modules
|
||||||
|
|||||||
62
.github/scripts/validate.js
vendored
@@ -25,28 +25,40 @@ const BOT_LABELS = [
|
|||||||
'Missing Repro',
|
'Missing Repro',
|
||||||
'Waiting for Review',
|
'Waiting for Review',
|
||||||
'Newer Version Available',
|
'Newer Version Available',
|
||||||
|
'6.x.x',
|
||||||
|
'7.0',
|
||||||
...Object.values(PLATFORM_LABELS),
|
...Object.values(PLATFORM_LABELS),
|
||||||
];
|
];
|
||||||
|
|
||||||
const SKIP_LABEL = 'No Validation';
|
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 = {
|
const MESSAGE = {
|
||||||
FEATURE_REQUEST: `Thank you for your feature request. We will review it and get back to you if we need more information.`,
|
FEATURE_REQUEST: (issueNumber) => `Thanks for the feature request! 🚀
|
||||||
BUG_REPORT: `Thank you for your bug report. We will review it and get back to you if we need more information.`,
|
\nYou can check out our [public roadmap](https://github.com/orgs/TheWidlarzGroup/projects/6) to see what we're currently working on. All requests are automatically added there, so you can track progress anytime.
|
||||||
|
\nWe review and implement new features when time allows, but this can take a while. If you'd like to speed things up and make this a priority, consider [Issue Boost](https://www.thewidlarzgroup.com/issue-boost/?utm_source=rnv&utm_medium=feature-request&utm_campaign=bot-message&utm_id=${issueNumber}), our commercial option that lets us dedicate time specifically to your request.
|
||||||
|
\nThanks for your input and patience! 🙌`,
|
||||||
|
BUG_REPORT: (issueNumber) => `Hey! 👋
|
||||||
|
Thanks for reporting this issue. We try to fix bugs as quickly as possible, but since our time is limited, we prioritize sponsored issues first, then focus on critical problems affecting many users, and finally, we handle other reports when we can. Some issues might take a while to be resolved.
|
||||||
|
\nIf you want to speed up this process, check out [Issue Boost](https://www.thewidlarzgroup.com/issue-boost/?utm_source=rnv&utm_medium=bug-report&utm_campaign=bot-message-valid&utm_id=${issueNumber}) – it allows us to dedicate time specifically to your issue and fix it faster.
|
||||||
|
\nThanks for your patience and support! 🚀`,
|
||||||
MISSING_INFO: (missingFields) => {
|
MISSING_INFO: (missingFields) => {
|
||||||
return `Thank you for your issue report. Please note that the following information is missing or incomplete:\n\n${missingFields
|
return `Hey! 👋
|
||||||
|
Thanks for the bug report. To help us resolve your issue effectively, we still need some key information:\n\n${missingFields
|
||||||
.map((field) => `- ${field.replace('missing-', '')}`)
|
.map((field) => `- ${field.replace('missing-', '')}`)
|
||||||
.join(
|
.join('\n')}
|
||||||
'\n',
|
|
||||||
)}\n\nPlease update your issue with this information to help us address it more effectively.
|
Please edit your issue and fill in the missing details.
|
||||||
\n > Note: issues without complete information have a lower priority`;
|
> Issues with incomplete info are treated with lower priority, so this helps speed things up.`;
|
||||||
},
|
},
|
||||||
OUTDATED_VERSION: (issueVersion, latestVersion) => {
|
OUTDATED_VERSION: (issueVersion, latestVersion) => {
|
||||||
return (
|
return (
|
||||||
`There is a newer version of the library available. ` +
|
`Heads up! ⚠️ You're using version **${issueVersion}**, but the latest stable version is **${latestVersion}**. ` +
|
||||||
`You are using version ${issueVersion}, while the latest stable version is ${latestVersion}. ` +
|
`Please update to the newest version and check if the issue still occurs.\n\n` +
|
||||||
`Please update to the latest version and check if the issue still exists.` +
|
`> Keeping your dependencies up-to-date often resolves many common problems.` +
|
||||||
`\n > Note: If the issue still exists, please update the issue report with the latest information.`
|
`\n\nStill having the issue after upgrading? Update the report with the new version details so we can investigate.`
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -114,6 +126,17 @@ const validateBugReport = async (body, labels) => {
|
|||||||
|
|
||||||
if (!isVersionValid) {
|
if (!isVersionValid) {
|
||||||
labels.add('missing-version');
|
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();
|
const latestVersion = await checkLatestVersion();
|
||||||
@@ -178,7 +201,8 @@ const handleIssue = async ({github, context}) => {
|
|||||||
const handleFeatureRequest = async ({github, context, body, labels}) => {
|
const handleFeatureRequest = async ({github, context, body, labels}) => {
|
||||||
validateFeatureRequest(body, labels);
|
validateFeatureRequest(body, labels);
|
||||||
|
|
||||||
const comment = MESSAGE.FEATURE_REQUEST;
|
const comment = MESSAGE.FEATURE_REQUEST(context.payload.issue.number);
|
||||||
|
await hidePreviousComments({github, context});
|
||||||
await createComment({github, context, body: comment});
|
await createComment({github, context, body: comment});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -212,6 +236,8 @@ const handleMissingInformation = async ({github, context, labels}) => {
|
|||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
comment += `\n\n${ISSUE_BOOST_INFO(context.payload.issue.number)}`;
|
||||||
|
|
||||||
await hidePreviousComments({github, context});
|
await hidePreviousComments({github, context});
|
||||||
await createComment({github, context, body: comment});
|
await createComment({github, context, body: comment});
|
||||||
}
|
}
|
||||||
@@ -220,7 +246,7 @@ const handleMissingInformation = async ({github, context, labels}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleValidReport = async ({github, context, labels}) => {
|
const handleValidReport = async ({github, context, labels}) => {
|
||||||
let comment = MESSAGE.BUG_REPORT;
|
let comment = MESSAGE.BUG_REPORT(context.payload.issue.number);
|
||||||
|
|
||||||
const outdatedVersionLabel = Array.from(labels).find((label) =>
|
const outdatedVersionLabel = Array.from(labels).find((label) =>
|
||||||
label.startsWith('outdated-version'),
|
label.startsWith('outdated-version'),
|
||||||
@@ -267,11 +293,15 @@ const hidePreviousComments = async ({github, context}) => {
|
|||||||
issue_number: context.payload.issue.number,
|
issue_number: context.payload.issue.number,
|
||||||
});
|
});
|
||||||
|
|
||||||
const botComments = comments.data.filter(
|
// Filter for bot comments that aren't already hidden
|
||||||
(comment) => comment.user.type === 'Bot',
|
const unhiddenBotComments = comments.data.filter(
|
||||||
|
(comment) =>
|
||||||
|
comment.user.type === 'Bot' &&
|
||||||
|
!comment.body.includes('<details>') &&
|
||||||
|
!comment.body.includes('Previous bot comment'),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const comment of botComments) {
|
for (const comment of unhiddenBotComments) {
|
||||||
// Don't format string - it will broke the markdown
|
// Don't format string - it will broke the markdown
|
||||||
const hiddenBody = `
|
const hiddenBody = `
|
||||||
<details>
|
<details>
|
||||||
|
|||||||
48
.github/workflows/build-android.yml
vendored
@@ -7,16 +7,16 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- '.github/workflows/build-android.yml'
|
- '.github/workflows/build-android.yml'
|
||||||
- 'android/**'
|
- 'android/**'
|
||||||
- 'examples/basic/android/**'
|
- 'examples/bare/android/**'
|
||||||
- 'yarn.lock'
|
- 'yarn.lock'
|
||||||
- 'examples/basic/yarn.lock'
|
- 'examples/bare/yarn.lock'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/build-android.yml'
|
- '.github/workflows/build-android.yml'
|
||||||
- 'android/**'
|
- 'android/**'
|
||||||
- 'examples/basic/android/**'
|
- 'examples/bare/android/**'
|
||||||
- 'yarn.lock'
|
- 'yarn.lock'
|
||||||
- 'examples/basic/yarn.lock'
|
- 'examples/bare/yarn.lock'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -32,13 +32,21 @@ jobs:
|
|||||||
java-version: 17
|
java-version: 17
|
||||||
java-package: jdk
|
java-package: jdk
|
||||||
|
|
||||||
- name: Install node_modules
|
- name: Install node_modules at Root
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
working-directory: examples/basic
|
working-directory: ./
|
||||||
|
|
||||||
|
- name: Build Library
|
||||||
|
run: yarn build
|
||||||
|
|
||||||
|
- name: Install node_modules at Example
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
with:
|
||||||
|
working-directory: examples/bare
|
||||||
|
|
||||||
- name: Restore Gradle cache
|
- name: Restore Gradle cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@@ -46,11 +54,11 @@ jobs:
|
|||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gradle-
|
${{ runner.os }}-gradle-
|
||||||
- name: Run Gradle Build for basic example
|
- name: Run Gradle Build for bare example
|
||||||
run: cd examples/basic/android && ./gradlew assembleDebug --build-cache && cd ../../..
|
run: cd examples/bare/android && ./gradlew assembleDebug --build-cache && cd ../../..
|
||||||
|
|
||||||
build-without-ads:
|
build-with-ads:
|
||||||
name: Build Android Example App Without Ads
|
name: Build Android Example App With Ads
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -62,13 +70,21 @@ jobs:
|
|||||||
java-version: 17
|
java-version: 17
|
||||||
java-package: jdk
|
java-package: jdk
|
||||||
|
|
||||||
- name: Install node_modules
|
- name: Install node_modules at Root
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
working-directory: examples/basic
|
working-directory: ./
|
||||||
|
|
||||||
|
- name: Build Library
|
||||||
|
run: yarn build
|
||||||
|
|
||||||
|
- name: Install node_modules at Example
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
with:
|
||||||
|
working-directory: examples/bare
|
||||||
|
|
||||||
- name: Restore Gradle cache
|
- name: Restore Gradle cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@@ -76,5 +92,5 @@ jobs:
|
|||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gradle-
|
${{ runner.os }}-gradle-
|
||||||
- name: Run Gradle Build for basic example
|
- name: Run Gradle Build for bare example
|
||||||
run: cd examples/basic/android && export RNV_SAMPLE_ENABLE_ADS=false && ./gradlew assembleDebug --build-cache && cd ../../..
|
run: cd examples/bare/android && export RNV_SAMPLE_ENABLE_ADS=true && ./gradlew assembleDebug --build-cache && cd ../../..
|
||||||
|
|||||||
108
.github/workflows/build-ios.yml
vendored
@@ -9,28 +9,37 @@ on:
|
|||||||
- '.github/workflows/build-ios.yml'
|
- '.github/workflows/build-ios.yml'
|
||||||
- 'ios/**'
|
- 'ios/**'
|
||||||
- '*.podspec'
|
- '*.podspec'
|
||||||
- 'examples/basic/ios/**'
|
- 'examples/bare/ios/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/build-ios.yml'
|
- '.github/workflows/build-ios.yml'
|
||||||
- 'ios/**'
|
- 'ios/**'
|
||||||
- '*.podspec'
|
- '*.podspec'
|
||||||
- 'examples/basic/ios/**'
|
- 'examples/bare/ios/**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build iOS Example App
|
name: Build iOS Example App
|
||||||
runs-on: macos-14 # This allow us to use Xcode 15.0.1 which is a lot faster - TODO change to "macos-latest" once it's out of beta
|
runs-on: macos-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: examples/basic/ios
|
working-directory: examples/bare/ios
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install node_modules
|
- name: Install node_modules at Root
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
working-directory: examples/basic
|
working-directory: ./
|
||||||
|
|
||||||
|
- name: Build Library
|
||||||
|
working-directory: ./
|
||||||
|
run: yarn build
|
||||||
|
|
||||||
|
- name: Install node_modules at Example
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
with:
|
||||||
|
working-directory: examples/bare
|
||||||
|
|
||||||
- name: Restore buildcache
|
- name: Restore buildcache
|
||||||
uses: mikehardy/buildcache-action@v2
|
uses: mikehardy/buildcache-action@v2
|
||||||
@@ -43,43 +52,58 @@ jobs:
|
|||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|
||||||
- name: Restore Pods cache
|
- name: Restore Pods cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
examples/basic/ios/Pods
|
examples/bare/ios/Pods
|
||||||
~/Library/Caches/CocoaPods
|
~/Library/Caches/CocoaPods
|
||||||
~/.cocoapods
|
~/.cocoapods
|
||||||
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pods-
|
${{ runner.os }}-pods-
|
||||||
|
|
||||||
|
- name: Generate Native Project
|
||||||
|
run: pod install
|
||||||
|
|
||||||
- name: Install Pods
|
- name: Install Pods
|
||||||
run: pod install
|
run: pod install
|
||||||
|
|
||||||
- name: Install xcpretty
|
- name: Install xcpretty
|
||||||
run: gem install xcpretty
|
run: gem install xcpretty
|
||||||
|
|
||||||
- name: Build App
|
- name: Build App
|
||||||
run: "set -o pipefail && xcodebuild \
|
run: "set -o pipefail && xcodebuild \
|
||||||
-derivedDataPath build -UseModernBuildSystem=YES \
|
-derivedDataPath build -UseModernBuildSystem=YES \
|
||||||
-workspace videoplayer.xcworkspace \
|
-workspace BareExample.xcworkspace \
|
||||||
-scheme videoplayer \
|
-scheme BareExample \
|
||||||
-sdk iphonesimulator \
|
-sdk iphonesimulator \
|
||||||
-configuration Debug \
|
-configuration Debug \
|
||||||
-destination 'platform=iOS Simulator,name=iPhone 14' \
|
-destination 'platform=iOS Simulator,name=iPhone 16' \
|
||||||
build \
|
build \
|
||||||
CODE_SIGNING_ALLOWED=NO | xcpretty"
|
CODE_SIGNING_ALLOWED=NO | xcpretty"
|
||||||
|
|
||||||
build-with-ads:
|
build-with-ads:
|
||||||
name: Build iOS Example App With Ads
|
name: Build iOS Example App With Ads
|
||||||
runs-on: macos-14 # This allow us to use Xcode 15.0.1 which is a lot faster - TODO change to "macos-latest" once it's out of beta
|
runs-on: macos-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: examples/basic/ios
|
working-directory: examples/bare/ios
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install node_modules
|
- name: Install node_modules at Root
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
working-directory: examples/basic
|
working-directory: ./
|
||||||
|
|
||||||
|
- name: Build Library
|
||||||
|
working-directory: ./
|
||||||
|
run: yarn build
|
||||||
|
|
||||||
|
- name: Install node_modules at Example
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
with:
|
||||||
|
working-directory: examples/bare
|
||||||
|
|
||||||
- name: Restore buildcache
|
- name: Restore buildcache
|
||||||
uses: mikehardy/buildcache-action@v2
|
uses: mikehardy/buildcache-action@v2
|
||||||
@@ -92,43 +116,58 @@ jobs:
|
|||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|
||||||
- name: Restore Pods cache
|
- name: Restore Pods cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
examples/basic/ios/Pods
|
examples/bare/ios/Pods
|
||||||
~/Library/Caches/CocoaPods
|
~/Library/Caches/CocoaPods
|
||||||
~/.cocoapods
|
~/.cocoapods
|
||||||
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pods-
|
${{ runner.os }}-pods-
|
||||||
|
|
||||||
|
- name: Generate Native Project
|
||||||
|
run: export RNV_SAMPLE_ENABLE_ADS=true && pod install
|
||||||
|
|
||||||
- name: Install Pods
|
- name: Install Pods
|
||||||
run: export RNV_SAMPLE_ENABLE_ADS=true && pod install
|
run: export RNV_SAMPLE_ENABLE_ADS=true && pod install
|
||||||
|
|
||||||
- name: Install xcpretty
|
- name: Install xcpretty
|
||||||
run: gem install xcpretty
|
run: gem install xcpretty
|
||||||
|
|
||||||
- name: Build App
|
- name: Build App
|
||||||
run: "set -o pipefail && export RNV_SAMPLE_ENABLE_ADS=true && xcodebuild \
|
run: "set -o pipefail && export RNV_SAMPLE_ENABLE_ADS=true && xcodebuild \
|
||||||
-derivedDataPath build -UseModernBuildSystem=YES \
|
-derivedDataPath build -UseModernBuildSystem=YES \
|
||||||
-workspace videoplayer.xcworkspace \
|
-workspace BareExample.xcworkspace \
|
||||||
-scheme videoplayer \
|
-scheme BareExample \
|
||||||
-sdk iphonesimulator \
|
-sdk iphonesimulator \
|
||||||
-configuration Debug \
|
-configuration Debug \
|
||||||
-destination 'platform=iOS Simulator,name=iPhone 14' \
|
-destination 'platform=iOS Simulator,name=iPhone 16' \
|
||||||
build \
|
build \
|
||||||
CODE_SIGNING_ALLOWED=NO | xcpretty"
|
CODE_SIGNING_ALLOWED=NO | xcpretty"
|
||||||
|
|
||||||
build-with-caching:
|
build-with-caching:
|
||||||
name: Build iOS Example App With Caching
|
name: Build iOS Example App With Caching
|
||||||
runs-on: macos-14 # This allow us to use Xcode 15.0.1 which is a lot faster - TODO change to "macos-latest" once it's out of beta
|
runs-on: macos-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: examples/basic/ios
|
working-directory: examples/bare/ios
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install node_modules
|
- name: Install node_modules at Root
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
working-directory: examples/basic
|
working-directory: ./
|
||||||
|
|
||||||
|
- name: Build Library
|
||||||
|
working-directory: ./
|
||||||
|
run: yarn build
|
||||||
|
|
||||||
|
- name: Install node_modules at Example
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
with:
|
||||||
|
working-directory: examples/bare
|
||||||
|
|
||||||
- name: Restore buildcache
|
- name: Restore buildcache
|
||||||
uses: mikehardy/buildcache-action@v2
|
uses: mikehardy/buildcache-action@v2
|
||||||
@@ -141,26 +180,35 @@ jobs:
|
|||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|
||||||
- name: Restore Pods cache
|
- name: Restore Pods cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
examples/basic/ios/Pods
|
examples/bare/ios/Pods
|
||||||
~/Library/Caches/CocoaPods
|
~/Library/Caches/CocoaPods
|
||||||
~/.cocoapods
|
~/.cocoapods
|
||||||
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pods-
|
${{ runner.os }}-pods-
|
||||||
|
|
||||||
|
- name: Install gem dependencies
|
||||||
|
run: bundle install
|
||||||
|
|
||||||
|
- name: Generate Native Project
|
||||||
|
run: export RNV_SAMPLE_VIDEO_CACHING=true && bundle exec pod install
|
||||||
|
|
||||||
- name: Install Pods
|
- name: Install Pods
|
||||||
run: export RNV_SAMPLE_VIDEO_CACHING=true && pod install
|
run: export RNV_SAMPLE_VIDEO_CACHING=true && bundle exec pod install
|
||||||
|
|
||||||
- name: Install xcpretty
|
- name: Install xcpretty
|
||||||
run: gem install xcpretty
|
run: gem install xcpretty
|
||||||
|
|
||||||
- name: Build App
|
- name: Build App
|
||||||
run: "set -o pipefail && export RNV_SAMPLE_VIDEO_CACHING=true && xcodebuild \
|
run: "set -o pipefail && export RNV_SAMPLE_VIDEO_CACHING=true && xcodebuild \
|
||||||
-derivedDataPath build -UseModernBuildSystem=YES \
|
-derivedDataPath build -UseModernBuildSystem=YES \
|
||||||
-workspace videoplayer.xcworkspace \
|
-workspace BareExample.xcworkspace \
|
||||||
-scheme videoplayer \
|
-scheme BareExample \
|
||||||
-sdk iphonesimulator \
|
-sdk iphonesimulator \
|
||||||
-configuration Debug \
|
-configuration Debug \
|
||||||
-destination 'platform=iOS Simulator,name=iPhone 14' \
|
-destination 'platform=iOS Simulator,name=iPhone 16' \
|
||||||
build \
|
build \
|
||||||
CODE_SIGNING_ALLOWED=NO | xcpretty"
|
CODE_SIGNING_ALLOWED=NO | xcpretty"
|
||||||
2
.github/workflows/check-android.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
report-path: ./android/build/*.xml
|
report-path: ./android/build/*.xml
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ktlint-report
|
name: ktlint-report
|
||||||
path: ./android/build/*.xml
|
path: ./android/build/*.xml
|
||||||
|
|||||||
46
.github/workflows/deploy-docs.yml
vendored
@@ -1,46 +0,0 @@
|
|||||||
|
|
||||||
name: deploy docs
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
paths:
|
|
||||||
- '.github/workflows/deploy-docs.yml'
|
|
||||||
- 'docs/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy-docs:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Setup
|
|
||||||
uses: ./.github/actions/setup-bun
|
|
||||||
with:
|
|
||||||
working-directory: ./docs
|
|
||||||
|
|
||||||
- name: Cache build
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
docs/.next/cache
|
|
||||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}-${{ hashFiles('**/package.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}
|
|
||||||
${{ runner.os }}-nextjs-
|
|
||||||
|
|
||||||
- name: Build docs
|
|
||||||
run: |
|
|
||||||
bun --cwd docs build
|
|
||||||
touch docs/out/.nojekyll
|
|
||||||
|
|
||||||
- name: Deploy docs to GitHub Pages
|
|
||||||
uses: JamesIves/github-pages-deploy-action@v4
|
|
||||||
with:
|
|
||||||
branch: gh-pages
|
|
||||||
folder: docs/out
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
35
.github/workflows/test-build-docs.yml
vendored
@@ -1,35 +0,0 @@
|
|||||||
|
|
||||||
name: Test Docs build
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- '.github/workflows/test-build-docs.yml'
|
|
||||||
- 'docs/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-docs:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Setup
|
|
||||||
uses: ./.github/actions/setup-bun
|
|
||||||
with:
|
|
||||||
working-directory: ./docs
|
|
||||||
|
|
||||||
- name: Cache build
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
docs/.next/cache
|
|
||||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}-${{ hashFiles('**/package.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}
|
|
||||||
${{ runner.os }}-nextjs-
|
|
||||||
|
|
||||||
- name: Build docs
|
|
||||||
run: |
|
|
||||||
bun --cwd docs build
|
|
||||||
touch docs/out/.nojekyll
|
|
||||||
2
.github/workflows/validate-issue.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
validate-and-label:
|
validate-and-label:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Validate Issue Template and Add Labels
|
- name: Validate Issue Template and Add Labels
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
|
|||||||
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[submodule "examples/react-native-offline-video-download-starter"]
|
||||||
|
path = examples/react-native-offline-video-download-starter
|
||||||
|
url = https://github.com/TheWidlarzGroup/react-native-offline-video-starter.git
|
||||||
|
branch = main
|
||||||
2
.husky/pre-push
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
echo "precommit"
|
||||||
|
yarn check-all
|
||||||
262
CHANGELOG.md
@@ -1,4 +1,262 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [6.19.2](https://github.com/moskalakamil/react-native-video/compare/v6.19.1...v6.19.2) (2026-04-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* replace `absoluteFillObject` with `absoluteFill` ([#4880](https://github.com/moskalakamil/react-native-video/issues/4880)) ([ae7ff4f](https://github.com/moskalakamil/react-native-video/commit/ae7ff4fa07ce9234575664a541bbd2f557ff051c))
|
||||||
|
|
||||||
|
## [6.19.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.19.0...v6.19.1) (2026-03-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **ios:** IMA ad container resizing on orientation change ([#4771](https://github.com/TheWidlarzGroup/react-native-video/issues/4771)) ([fc936c4](https://github.com/TheWidlarzGroup/react-native-video/commit/fc936c49ef3c2734173442042b7d5038aeaef301))
|
||||||
|
* RCTVideoManager crash in bridgeless mode RN0.84 ([#4855](https://github.com/TheWidlarzGroup/react-native-video/issues/4855)) ([92b0a0e](https://github.com/TheWidlarzGroup/react-native-video/commit/92b0a0e416c7f313120a811cd2dc972f87b1c82f))
|
||||||
|
|
||||||
|
# [6.19.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.18.0...v6.19.0) (2026-01-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **android:** correct videoTrack type definitions to match Android implementation ([#4778](https://github.com/TheWidlarzGroup/react-native-video/issues/4778)) ([commit](https://github.com/TheWidlarzGroup/react-native-video/commit/f38717778515b06c462fe75dcd94d2cce5ba3f95))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **BREAKING CHANGE:** add DAI support ([#4816](https://github.com/TheWidlarzGroup/react-native-video/issues/4816)) ([commit](https://github.com/TheWidlarzGroup/react-native-video/commit/88ac1ae1dcdc907415f806bd64bd3d0a92ccd7d1))
|
||||||
|
|
||||||
|
# [6.18.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.17.0...v6.18.0) (2025-11-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **android:** prevent duplicate `onVideoEnd` callback on prop changes ([#4762](https://github.com/TheWidlarzGroup/react-native-video/issues/4762)) ([05cd597](https://github.com/TheWidlarzGroup/react-native-video/commit/05cd5972c21ebcacf3cd5952e92f121a84b5c9a9))
|
||||||
|
* **ci:** update ios device for builds ([#4757](https://github.com/TheWidlarzGroup/react-native-video/issues/4757)) ([a9f7524](https://github.com/TheWidlarzGroup/react-native-video/commit/a9f752435f3d94abfdb37ef5dc9c444038e3ad6a))
|
||||||
|
* entering PiP mode when controls are true ([#4776](https://github.com/TheWidlarzGroup/react-native-video/issues/4776)) ([ba65ab1](https://github.com/TheWidlarzGroup/react-native-video/commit/ba65ab123321713e537fc7ccb1ac3ed5676f1677))
|
||||||
|
* **iOS:** use top-most presented view controller for fullscreen presentation on iOS ([#4753](https://github.com/TheWidlarzGroup/react-native-video/issues/4753)) ([5d75b48](https://github.com/TheWidlarzGroup/react-native-video/commit/5d75b482952a9cd3e5f59237e302137857739d4e))
|
||||||
|
* prevent `audiovisualBackgroundPlaybackPolicy` crash ([#4763](https://github.com/TheWidlarzGroup/react-native-video/issues/4763)) ([fbb260e](https://github.com/TheWidlarzGroup/react-native-video/commit/fbb260e9164194a55d2b26404aea000e924e2f04))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **ios:** add PublicAudioSessionManager for audio session management ([#4747](https://github.com/TheWidlarzGroup/react-native-video/issues/4747)) ([f2afd16](https://github.com/TheWidlarzGroup/react-native-video/commit/f2afd16d0bc7fc72e0b4d8400d74342244158674))
|
||||||
|
|
||||||
|
# [6.17.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.16.1...v6.17.0) (2025-10-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* android control option is not work when init video ([#4698](https://github.com/TheWidlarzGroup/react-native-video/issues/4698)) ([5074ca5](https://github.com/TheWidlarzGroup/react-native-video/commit/5074ca5a1b1ae687255f3509fd75cc847ace0dca))
|
||||||
|
* **android:** catch errors from `activity.packageManager` ([5b59b06](https://github.com/TheWidlarzGroup/react-native-video/commit/5b59b06d1e19f138a98648d06a931ab4c634cb75))
|
||||||
|
* **ios:** add audiovisualBackgroundPlaybackPolicy ([#4570](https://github.com/TheWidlarzGroup/react-native-video/issues/4570)) ([bf81038](https://github.com/TheWidlarzGroup/react-native-video/commit/bf810386b9ee1aed579b24adffb02f4acfa413db))
|
||||||
|
* **ios:** disable audio session management when no views ([#4666](https://github.com/TheWidlarzGroup/react-native-video/issues/4666)) ([d2c92a1](https://github.com/TheWidlarzGroup/react-native-video/commit/d2c92a1f3f6579aa6607389de8e51e4b75012f3b))
|
||||||
|
* **player:** trigger relayout after exiting pip ([#4665](https://github.com/TheWidlarzGroup/react-native-video/issues/4665)) ([4b996fc](https://github.com/TheWidlarzGroup/react-native-video/commit/4b996fc514b015dfb848dd2a0226cda4b0c527fb))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **android:** enable flexible page sizes in native build configuration ([#4691](https://github.com/TheWidlarzGroup/react-native-video/issues/4691)) ([7d233f4](https://github.com/TheWidlarzGroup/react-native-video/commit/7d233f414f8a00734d3991f2e1d148bbadae0f0e))
|
||||||
|
* **android:** report full native stack trace on error ([#4651](https://github.com/TheWidlarzGroup/react-native-video/issues/4651)) ([724f639](https://github.com/TheWidlarzGroup/react-native-video/commit/724f63930d2de3c81b9d9316ae70bbb9e1dcae49))
|
||||||
|
* **examples:** add offline-video-starter as submodule ([#4644](https://github.com/TheWidlarzGroup/react-native-video/issues/4644)) ([60baecd](https://github.com/TheWidlarzGroup/react-native-video/commit/60baecdf739baa3ce9a84b3a02cd4e5b9060275f))
|
||||||
|
|
||||||
|
## [6.16.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.16.0...v6.16.1) (2025-07-08)
|
||||||
|
|
||||||
|
# [6.16.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.15.0...v6.16.0) (2025-07-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **android:** create custom event class for dispatcher ([#4575](https://github.com/TheWidlarzGroup/react-native-video/issues/4575)) ([94967fc](https://github.com/TheWidlarzGroup/react-native-video/commit/94967fc4a61e00c521242d29fd28bed13bbba7c4))
|
||||||
|
* **android:** speed control is not applied ([#4572](https://github.com/TheWidlarzGroup/react-native-video/issues/4572)) ([b56b647](https://github.com/TheWidlarzGroup/react-native-video/commit/b56b647d85aa24df2ba2ad436eb5ada67735417f))
|
||||||
|
* **ios:** allow audio mixing when none of the players are playing ([#4579](https://github.com/TheWidlarzGroup/react-native-video/issues/4579)) ([2d7e039](https://github.com/TheWidlarzGroup/react-native-video/commit/2d7e03942d5e2525e5c71dc914926ba07f67d54a))
|
||||||
|
* **ios:** retain cycle and memory leak involving the RCTVideo instance when using Google IMA ads in react-native-video. ([#4574](https://github.com/TheWidlarzGroup/react-native-video/issues/4574)) ([b51b579](https://github.com/TheWidlarzGroup/react-native-video/commit/b51b579ff00067231fb1d0754e1a86692f44bc50))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **android:** replace custom VideoView with media3 `PlayerView` ([#4581](https://github.com/TheWidlarzGroup/react-native-video/issues/4581)) ([978683b](https://github.com/TheWidlarzGroup/react-native-video/commit/978683b64582e6363d7b5a1817e22fec342d1c47))
|
||||||
|
|
||||||
|
# [6.15.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.14.1...v6.15.0) (2025-06-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** add missing component type ([9f03cc5](https://github.com/TheWidlarzGroup/react-native-video/commit/9f03cc5a0983e05515d3d194204a7c9eb71b383d))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* allow audio mixing if one of the video views require it ([#4559](https://github.com/TheWidlarzGroup/react-native-video/issues/4559)) ([3576a13](https://github.com/TheWidlarzGroup/react-native-video/commit/3576a134e678e6273d9195d7a17c9828c30426e2))
|
||||||
|
* **ios:** forward real fullscreen events from AVPlayer instead of guessing ([#4509](https://github.com/TheWidlarzGroup/react-native-video/issues/4509)) ([88c20d1](https://github.com/TheWidlarzGroup/react-native-video/commit/88c20d1c065b2778a66ddde4a3b92bd58bec043b))
|
||||||
|
* **plugin:** overrideMediaSourceFactory ([#4566](https://github.com/TheWidlarzGroup/react-native-video/issues/4566)) ([9cf7802](https://github.com/TheWidlarzGroup/react-native-video/commit/9cf780276af7f0a890025a13e1685e49c6589f0e))
|
||||||
|
|
||||||
|
## [6.14.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.14.0...v6.14.1) (2025-05-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **android:** allow plugins to override drm session manager ([#4558](https://github.com/TheWidlarzGroup/react-native-video/issues/4558)) ([9449eb3](https://github.com/TheWidlarzGroup/react-native-video/commit/9449eb34f3ebcf7ac08bde1ee55e5cc26a142217))
|
||||||
|
|
||||||
|
# [6.14.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.13.0...v6.14.0) (2025-05-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **ios:** default cropStart fallback ([#4540](https://github.com/TheWidlarzGroup/react-native-video/issues/4540)) ([ecfe12a](https://github.com/TheWidlarzGroup/react-native-video/commit/ecfe12aa816a3126e023d0c14896681d92fe8926))
|
||||||
|
* **ios:** set `_isBuffering = true` only if newValue is also true ([#4532](https://github.com/TheWidlarzGroup/react-native-video/issues/4532)) ([089e938](https://github.com/TheWidlarzGroup/react-native-video/commit/089e938aebc222378f4d16006f585f20a8b0eed1))
|
||||||
|
* **ios:** swfit modular headers ([#4527](https://github.com/TheWidlarzGroup/react-native-video/issues/4527)) ([987be4b](https://github.com/TheWidlarzGroup/react-native-video/commit/987be4b293dc21a40b0b183433707bc9a733b715))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **web:** allow `style` prop overrides ([#4528](https://github.com/TheWidlarzGroup/react-native-video/issues/4528)) ([fc1e3f4](https://github.com/TheWidlarzGroup/react-native-video/commit/fc1e3f4fd17faf1503b6b3d7cc604dbe1bc659c1))
|
||||||
|
|
||||||
|
# [6.13.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.12.0...v6.13.0) (2025-04-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **macCatalyst:** allowsVideoFrameAnalysis not available in catalyst until 18.0 ([#4515](https://github.com/TheWidlarzGroup/react-native-video/issues/4515)) ([b17c319](https://github.com/TheWidlarzGroup/react-native-video/commit/b17c319c32ad8080ad911ec4be7fa02bc4a6d6ee))
|
||||||
|
* **tvos:** build ([#4511](https://github.com/TheWidlarzGroup/react-native-video/issues/4511)) ([4034046](https://github.com/TheWidlarzGroup/react-native-video/commit/40340467d7df2ad606f25606041630c489c385f6))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **ios:** add `overridePlayerAsset` to `AVPlayerPlugin` ([#4522](https://github.com/TheWidlarzGroup/react-native-video/issues/4522)) ([b1b3db3](https://github.com/TheWidlarzGroup/react-native-video/commit/b1b3db301097e82c8d449c08f7d334a2eaeac0ea))
|
||||||
|
|
||||||
|
# [6.12.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.11.0...v6.12.0) (2025-04-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add extra checks to determine if a file is local ([#4503](https://github.com/TheWidlarzGroup/react-native-video/issues/4503)) ([a849cc1](https://github.com/TheWidlarzGroup/react-native-video/commit/a849cc19e8dafe0cc8147aeb8a226fc1373bb84d))
|
||||||
|
* **ios:** don't try to modify react view after unmount ([#4474](https://github.com/TheWidlarzGroup/react-native-video/issues/4474)) ([229a576](https://github.com/TheWidlarzGroup/react-native-video/commit/229a5764ea45ea74e5d469aaac30057c201bd228))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **android:** add asset management functions to plugin ([#4494](https://github.com/TheWidlarzGroup/react-native-video/issues/4494)) ([697afd5](https://github.com/TheWidlarzGroup/react-native-video/commit/697afd52f60cbd52660843b8d29228a2ff7d0060))
|
||||||
|
* **android:** initial bitrate ([#4480](https://github.com/TheWidlarzGroup/react-native-video/issues/4480)) ([41ddc5c](https://github.com/TheWidlarzGroup/react-native-video/commit/41ddc5c27a9c180a6d0364b3f3286211f7a10e68))
|
||||||
|
* **ios:** allow to disable audio sessions management ([#4492](https://github.com/TheWidlarzGroup/react-native-video/issues/4492)) ([8836362](https://github.com/TheWidlarzGroup/react-native-video/commit/8836362609d226f795b01e1c65a70b9c4ecde1e5))
|
||||||
|
* **ios:** set playback speed controls to initial playback rate ([#4495](https://github.com/TheWidlarzGroup/react-native-video/issues/4495)) ([d2e5d9c](https://github.com/TheWidlarzGroup/react-native-video/commit/d2e5d9c64eeb80143c39ca39fcdc8ba65f40a400))
|
||||||
|
|
||||||
|
# [6.11.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.10.2...v6.11.0) (2025-03-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **android:** call `startForeground()` immediately to prevent `ForegroundServiceDidNotStartInTimeException` ([#4453](https://github.com/TheWidlarzGroup/react-native-video/issues/4453)) ([b510374](https://github.com/TheWidlarzGroup/react-native-video/commit/b5103743e87a6ef24c69a07a8368415ff0fc7886))
|
||||||
|
* **android:** fix bugs related Android PIP listeners ([#4441](https://github.com/TheWidlarzGroup/react-native-video/issues/4441)) ([82f5f3d](https://github.com/TheWidlarzGroup/react-native-video/commit/82f5f3d21c2f29282c024bfa3a8f9562a4ed8e0f))
|
||||||
|
* **android:** prevent ratio calculations before loading video ([#4442](https://github.com/TheWidlarzGroup/react-native-video/issues/4442)) ([235c281](https://github.com/TheWidlarzGroup/react-native-video/commit/235c28121903e20f58d4d830fea5929d2e6e0b56))
|
||||||
|
* **ios:** invalid metadata handling ([#4422](https://github.com/TheWidlarzGroup/react-native-video/issues/4422)) ([bc533e5](https://github.com/TheWidlarzGroup/react-native-video/commit/bc533e53b0b4c6906f6e72eac7e3f228da3e4bbd))
|
||||||
|
* **tvOS:** handle allowsPictureInPicturePlayback for tvOS ([#4448](https://github.com/TheWidlarzGroup/react-native-video/issues/4448)) ([057c287](https://github.com/TheWidlarzGroup/react-native-video/commit/057c287f127b9f796e592ae538478386f42a447c))
|
||||||
|
* **windows:** event name not matches with SPEC ([#4455](https://github.com/TheWidlarzGroup/react-native-video/issues/4455)) ([fa20223](https://github.com/TheWidlarzGroup/react-native-video/commit/fa20223c4498e72bd13b87b05c0297b544c4724b))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* enhance react-native-video plugins [Plugins API Breaking] ([#4366](https://github.com/TheWidlarzGroup/react-native-video/issues/4366)) ([6e6f915](https://github.com/TheWidlarzGroup/react-native-video/commit/6e6f91517c492cdc7d2140ae8564712e9a98450a))
|
||||||
|
* **windows:** add topSeek parms mentioned in docs ([#4456](https://github.com/TheWidlarzGroup/react-native-video/issues/4456)) ([d902c1b](https://github.com/TheWidlarzGroup/react-native-video/commit/d902c1bf4390a5503eb0ff924ec7cafb2b08a428))
|
||||||
|
|
||||||
|
## [6.10.2](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.10.1...v6.10.2) (2025-02-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **android:** fix gradle exception text related to AndroidX version ([#4420](https://github.com/TheWidlarzGroup/react-native-video/issues/4420)) ([6697cbf](https://github.com/TheWidlarzGroup/react-native-video/commit/6697cbf5d07ce5cc882637acc1249192f6189e53))
|
||||||
|
* **tvOS:** fix tvos compile error for rotation handler ([#4417](https://github.com/TheWidlarzGroup/react-native-video/issues/4417)) ([04eec42](https://github.com/TheWidlarzGroup/react-native-video/commit/04eec42f1e0f8b8f25dc1c50e1e57e62ca8c3356))
|
||||||
|
|
||||||
|
## [6.10.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.10.0...v6.10.1) (2025-02-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **android:** check androidX version at early build process before really launching build ([#4388](https://github.com/TheWidlarzGroup/react-native-video/issues/4388)) ([638f454](https://github.com/TheWidlarzGroup/react-native-video/commit/638f454a2118873de23a14f673848f14eeb10122))
|
||||||
|
* **android:** check for valid width and height on video format data ([#4394](https://github.com/TheWidlarzGroup/react-native-video/issues/4394)) ([ad52668](https://github.com/TheWidlarzGroup/react-native-video/commit/ad52668d0541a2d29b1ee8087f9d25cc6a5e8ab2))
|
||||||
|
* **android:** remove transparent black overlay on android default controls ([#4392](https://github.com/TheWidlarzGroup/react-native-video/issues/4392)) ([74b1d5b](https://github.com/TheWidlarzGroup/react-native-video/commit/74b1d5b540a062371689f619f1843807ab7ca0e5))
|
||||||
|
* **infra:** kotlin linter github action ([#4408](https://github.com/TheWidlarzGroup/react-native-video/issues/4408)) ([2905b61](https://github.com/TheWidlarzGroup/react-native-video/commit/2905b61a0d993715cdf94d9f85f949c9e64d098b))
|
||||||
|
* **ios:** fix constraints when controls are enabled and video is inside a ScrollView ([#4383](https://github.com/TheWidlarzGroup/react-native-video/issues/4383)) ([a8ca97f](https://github.com/TheWidlarzGroup/react-native-video/commit/a8ca97f05fd52ec331fd8fe0e7c2375956a76c2c))
|
||||||
|
* **ios:** the video has no audio by default ([#4409](https://github.com/TheWidlarzGroup/react-native-video/issues/4409)) ([c8b800a](https://github.com/TheWidlarzGroup/react-native-video/commit/c8b800a508f51a4488b7b260a6bf573b5b6d44d9)), closes [#4400](https://github.com/TheWidlarzGroup/react-native-video/issues/4400)
|
||||||
|
* **sample:** remove duplicate code in sample ([#4391](https://github.com/TheWidlarzGroup/react-native-video/issues/4391)) ([faac5ad](https://github.com/TheWidlarzGroup/react-native-video/commit/faac5ad45689a605045dae57061d6648244c7dad))
|
||||||
|
|
||||||
|
# [6.10.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.9.1...v6.10.0) (2025-01-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **android:** support RN 0.77 ([#4386](https://github.com/TheWidlarzGroup/react-native-video/issues/4386)) ([8b952e7](https://github.com/TheWidlarzGroup/react-native-video/commit/8b952e709a6535325ffbfab9358538d06b308a15))
|
||||||
|
* **ios:** fix paused video by default ([#4379](https://github.com/TheWidlarzGroup/react-native-video/issues/4379)) ([3d3eba9](https://github.com/TheWidlarzGroup/react-native-video/commit/3d3eba97e814519cd79836208af25b948059795b))
|
||||||
|
* **tvOS:** fix tvos compile error ([#4369](https://github.com/TheWidlarzGroup/react-native-video/issues/4369)) ([6c3af99](https://github.com/TheWidlarzGroup/react-native-video/commit/6c3af99979c847887796baefd421cb5a50ed32ba))
|
||||||
|
* **windows:** conversion of string to Stretch enum ([#4381](https://github.com/TheWidlarzGroup/react-native-video/issues/4381)) ([d90bf47](https://github.com/TheWidlarzGroup/react-native-video/commit/d90bf47df58e61a8946f4762659f3e5613aba95d))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **web:** implement web pip method and event ([#4370](https://github.com/TheWidlarzGroup/react-native-video/issues/4370)) ([8dc10fd](https://github.com/TheWidlarzGroup/react-native-video/commit/8dc10fd4b774b2cc91bf75f4f1680b63411b8559))
|
||||||
|
|
||||||
|
## [6.9.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.9.0...v6.9.1) (2025-01-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* avoid memory leak on iOS ([#4355](https://github.com/TheWidlarzGroup/react-native-video/issues/4355)) ([424f4ee](https://github.com/TheWidlarzGroup/react-native-video/commit/424f4eeddea989392e25c52f45a9a0281ead6fe1))
|
||||||
|
* NPE in setEnterPictureInPictureOnLeave for unsupported Android versions ([#4362](https://github.com/TheWidlarzGroup/react-native-video/issues/4362)) ([3924b5e](https://github.com/TheWidlarzGroup/react-native-video/commit/3924b5e295ed64c97284f4665bc294066a83574a))
|
||||||
|
|
||||||
|
# [6.9.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.8.2...v6.9.0) (2025-01-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **android:** disable caching on local asset files ([#4304](https://github.com/TheWidlarzGroup/react-native-video/issues/4304)) ([63c592f](https://github.com/TheWidlarzGroup/react-native-video/commit/63c592f7cd897caf918fd3bd5f129c72432d2b55))
|
||||||
|
* **docs:** bump `next.js` version & fix meta warnings ([#4327](https://github.com/TheWidlarzGroup/react-native-video/issues/4327)) ([7b4bd9a](https://github.com/TheWidlarzGroup/react-native-video/commit/7b4bd9a0169fc2ea6f277dd7ed904bada98bc63a))
|
||||||
|
* hiding poster ([#4308](https://github.com/TheWidlarzGroup/react-native-video/issues/4308)) ([621a802](https://github.com/TheWidlarzGroup/react-native-video/commit/621a80299c690c07846f3fcd8a6c73b7ecde39bf))
|
||||||
|
* **ios:** `_paused` is updated when video playback pause ([#4320](https://github.com/TheWidlarzGroup/react-native-video/issues/4320)) ([3da4f1c](https://github.com/TheWidlarzGroup/react-native-video/commit/3da4f1ca979058b387b1be2c2141f6b93fd084a7))
|
||||||
|
* **ios:** disables subtitles for `none` and `empty` track types ([#4319](https://github.com/TheWidlarzGroup/react-native-video/issues/4319)) ([1033c9d](https://github.com/TheWidlarzGroup/react-native-video/commit/1033c9d4f3db7042a96e7108a7fe9f1567d69ded))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* implement enterPictureInPictureOnLeave prop for both platform (Android, iOS) ([#3385](https://github.com/TheWidlarzGroup/react-native-video/issues/3385)) ([69a7bc2](https://github.com/TheWidlarzGroup/react-native-video/commit/69a7bc2d265f2cf4985f8d81054c46f47ee3bae2))
|
||||||
|
|
||||||
|
## [6.8.2](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.8.1...v6.8.2) (2024-11-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* playback restart without bufferingConfig ([#4305](https://github.com/TheWidlarzGroup/react-native-video/issues/4305)) ([f37dc9e](https://github.com/TheWidlarzGroup/react-native-video/commit/f37dc9e33ebefd922605c5ae91360379fe91bed6))
|
||||||
|
|
||||||
|
## [6.8.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.8.0...v6.8.1) (2024-11-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **ios:** handle async player access in text track selection ([#4293](https://github.com/TheWidlarzGroup/react-native-video/issues/4293)) ([daaac97](https://github.com/TheWidlarzGroup/react-native-video/commit/daaac9740aed1858b7ababae0ec8b08274130a27))
|
||||||
|
|
||||||
|
# [6.8.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.7.0...v6.8.0) (2024-11-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **android:** add helper to avoid type error ([#4257](https://github.com/TheWidlarzGroup/react-native-video/issues/4257)) ([3b4bfd3](https://github.com/TheWidlarzGroup/react-native-video/commit/3b4bfd3936a8cb846c0e61ffd396940987a7ba43))
|
||||||
|
|
||||||
|
# [6.7.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.6.4...v6.7.0) (2024-10-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **android:** sideloaded subtitles ([#4232](https://github.com/TheWidlarzGroup/react-native-video/issues/4232)) ([352dfbb](https://github.com/TheWidlarzGroup/react-native-video/commit/352dfbbc9bef158400c59516a50d889d25757c0d))
|
||||||
|
* ensure aspect ratio from video is handled in a coherent way ([#4219](https://github.com/TheWidlarzGroup/react-native-video/issues/4219)) ([a8d5841](https://github.com/TheWidlarzGroup/react-native-video/commit/a8d5841c7c0f9767ec095ffd8401b1579f32623f))
|
||||||
|
* **iOS:** pause video on end reached & don't remove listeners ([#4218](https://github.com/TheWidlarzGroup/react-native-video/issues/4218)) ([2c19a47](https://github.com/TheWidlarzGroup/react-native-video/commit/2c19a4770df73179436a9e23a5e55ad0699fcfcc))
|
||||||
|
* remove warning and refactor & fix ad workflow ([#4235](https://github.com/TheWidlarzGroup/react-native-video/issues/4235)) ([7501880](https://github.com/TheWidlarzGroup/react-native-video/commit/7501880062a4c381838949084f0017a2aecc58d7))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add setSource API function fix ads playback ([#4185](https://github.com/TheWidlarzGroup/react-native-video/issues/4185)) ([9a3fcda](https://github.com/TheWidlarzGroup/react-native-video/commit/9a3fcda3b8ca4689c9131a12a8375fc43d442f80))
|
||||||
|
* **android:** add settings button to control video playback speed ([#4211](https://github.com/TheWidlarzGroup/react-native-video/issues/4211)) ([d1883a7](https://github.com/TheWidlarzGroup/react-native-video/commit/d1883a7e008706cac2a2beac934194539c9b5b77))
|
||||||
|
* **exoplayerview:** Migrate ExoPlayerView to kotlin ([#4038](https://github.com/TheWidlarzGroup/react-native-video/issues/4038)) ([78f4f04](https://github.com/TheWidlarzGroup/react-native-video/commit/78f4f0480d70d209fea9e0579963e347c965fd6e))
|
||||||
|
|
||||||
## [6.6.4](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.6.3...v6.6.4) (2024-10-03)
|
## [6.6.4](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.6.3...v6.6.4) (2024-10-03)
|
||||||
|
|
||||||
@@ -514,7 +772,7 @@
|
|||||||
* add release-it ([#3342](https://github.com/react-native-video/react-native-video/issues/3342)) ([da27089](https://github.com/react-native-video/react-native-video/commit/da270891fbce485bb132825a336638f2af98408d))
|
* 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))
|
* **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
|
## Next
|
||||||
- Android, iOS: add onVolumeChange event #3322
|
- Android, iOS: add onVolumeChange event #3322
|
||||||
@@ -863,4 +1121,4 @@
|
|||||||
- Add cookie support for ExoPlayer [#922](https://github.com/react-native-community/react-native-video/pull/922)
|
- Add cookie support for ExoPlayer [#922](https://github.com/react-native-community/react-native-video/pull/922)
|
||||||
- Remove ExoPlayer onMetadata that wasn't being used [#1040](https://github.com/react-native-community/react-native-video/pull/1040)
|
- Remove ExoPlayer onMetadata that wasn't being used [#1040](https://github.com/react-native-community/react-native-video/pull/1040)
|
||||||
- Fix bug where setting the progress interval on iOS didn't work [#800](https://github.com/react-native-community/react-native-video/pull/800)
|
- Fix bug where setting the progress interval on iOS didn't work [#800](https://github.com/react-native-community/react-native-video/pull/800)
|
||||||
- Support setting the poster resize mode [#595](https://github.com/react-native-community/react-native-video/pull/595)
|
- Support setting the poster resize mode [#595](https://github.com/react-native-community/react-native-video/pull/595)
|
||||||
|
|||||||
@@ -24,8 +24,30 @@ For example, a single file JS code change requires 1 review while a 3 files iOS
|
|||||||
|
|
||||||
* If you have time to help out, look for the [`review requested`](https://github.com/TheWidlarzGroup/react-native-video/labels/review%20requested) label. It will have another numeric label with it (`1`, `2`, or `3` indicating how many more reviews are needed to merge).
|
* If you have time to help out, look for the [`review requested`](https://github.com/TheWidlarzGroup/react-native-video/labels/review%20requested) label. It will have another numeric label with it (`1`, `2`, or `3` indicating how many more reviews are needed to merge).
|
||||||
|
|
||||||
## Releases
|
|
||||||
|
|
||||||
* Aim for a bi-weekly (every other week) release to flush out whatever was approved and merge. Most people use this with a lock file (and if you don't you are doing it wrong) and should not have any issues with new bugs showing up. This is already a high risk dependency which must be tested well before going into production. Let's take advantage of that and move faster.
|
|
||||||
|
|
||||||
Please do not harass people to review your pull request! You can tag those you feel have relevant experience but please don't abuse this as people will unfollow or mute the project if they are called too many times!
|
Please do not harass people to review your pull request! You can tag those you feel have relevant experience but please don't abuse this as people will unfollow or mute the project if they are called too many times!
|
||||||
|
|
||||||
|
### Running the example
|
||||||
|
|
||||||
|
To see how to run examples locally, please refer to the [examples guide](https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples)
|
||||||
|
|
||||||
|
### Working on documentation
|
||||||
|
|
||||||
|
The documentation is located in the `docs` folder. To work on the documentation, you can run the following command to start a local server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd docs
|
||||||
|
bun install
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Publishing a release
|
||||||
|
|
||||||
|
We use [release-it](https://github.com/webpro/release-it) to automate our release.
|
||||||
|
|
||||||
|
## Reporting issues
|
||||||
|
|
||||||
|
You can report issues on our [bug tracker](https://github.com/TheWidlarzGroup/react-native-video/issues). Please follow the issue template when opening an issue.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing to React Native Video, you agree that your contributions will be licensed under its **MIT** license.
|
||||||
|
|||||||
211
README.md
@@ -1,62 +1,173 @@
|
|||||||
# react-native-video
|
[](https://thewidlarzgroup.com/?utm_source=rnv&utm_medium=readme&utm_id=banner)
|
||||||
🎬 `<Video>` component for React Native
|
|
||||||
|
|
||||||
## Documentation
|
The most battle-tested open-source video player component for React Native with support for DRM, offline playback, HLS/DASH streaming, and more.
|
||||||
documentation is available at [thewidlarzgroup.github.io/react-native-video/](https://thewidlarzgroup.github.io/react-native-video/)
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```javascript
|
## 🔍 Features
|
||||||
// Load the module
|
|
||||||
|
|
||||||
import Video, {VideoRef} from 'react-native-video';
|
- 📱 Plays all video formats natively supported by iOS/Android
|
||||||
|
- ▶️ Local and remote playback
|
||||||
|
- 🔁 Streaming: HLS • DASH • SmoothStreaming
|
||||||
|
- 🔐 DRM: Widevine & FairPlay ([See free DRM stream example](https://www.thewidlarzgroup.com/services/free-drm-token-generator-for-video?utm_source=rnv&utm_medium=readme&utm_id=free-drm))
|
||||||
|
- 📴 Offline playback, video download, support for side-tracks and side-captions (via [optional SDK](https://docs.thewidlarzgroup.com/offline-video-sdk?utm_source=rnv&utm_medium=readme&utm_id=features-text))
|
||||||
|
- 🎚️ Fine-grained control over tracks, buffering & events
|
||||||
|
- 🧩 Expo plugin support
|
||||||
|
- 🌐 Basic Web Support
|
||||||
|
- 📱 Picture in Picture
|
||||||
|
- 📺 TV Support
|
||||||
|
|
||||||
// Within your render function, assuming you have a file called
|
|
||||||
// "background.mp4" in your project. You can include multiple videos
|
|
||||||
// on a single screen if you like.
|
|
||||||
|
|
||||||
const VideoPlayer = () => {
|
|
||||||
const videoRef = useRef<VideoRef>(null);
|
|
||||||
const background = require('./background.mp4');
|
|
||||||
|
|
||||||
return (
|
## ✨ Project Status
|
||||||
<Video
|
|
||||||
// Can be a URL or a local file.
|
|
||||||
source={background}
|
|
||||||
// Store reference
|
|
||||||
ref={videoRef}
|
|
||||||
// Callback when remote video is buffering
|
|
||||||
onBuffer={onBuffer}
|
|
||||||
// Callback when video cannot be loaded
|
|
||||||
onError={onError}
|
|
||||||
style={styles.backgroundVideo}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Later on in your styles..
|
| Version | State | Architecture |
|
||||||
var styles = StyleSheet.create({
|
|---------|-------|--------------|
|
||||||
backgroundVideo: {
|
| **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 |
|
||||||
position: 'absolute',
|
| **v6** | 🛠 Maintained (community + TWG) | Old + New (Interop Layer) |
|
||||||
top: 0,
|
| [**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) |
|
||||||
left: 0,
|
|
||||||
bottom: 0,
|
[`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.
|
||||||
right: 0,
|
|
||||||
},
|
---
|
||||||
});
|
|
||||||
|
## 📚 Documentation & Examples
|
||||||
|
|
||||||
|
- 📖 [Documentation](https://docs.thewidlarzgroup.com/react-native-video/)
|
||||||
|
- 📦 [Example: Free DRM Stream](https://www.thewidlarzgroup.com/services/free-drm-token-generator-for-video?utm_source=rnv&utm_medium=readme&utm_id=free-drm)
|
||||||
|
- 📦 [Example: Offline SDK integration](https://docs.thewidlarzgroup.com/offline-video-sdk)
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Install
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
yarn add react-native-video
|
||||||
|
|
||||||
|
# Install pods
|
||||||
|
cd ios && pod install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Community support
|
### Usage
|
||||||
We have an discord server where you can ask questions and get help. [Join the discord server](https://discord.gg/WXuM4Tgb9X)
|
```tsx
|
||||||
|
import Video from 'react-native-video';
|
||||||
|
|
||||||
## Enterprise Support
|
export default () => (
|
||||||
<p>
|
<Video
|
||||||
📱 <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! 🚀
|
source={{ uri: 'https://www.w3schools.com/html/mov_bbb.mp4' }}
|
||||||
</p>
|
style={{ width: '100%', aspectRatio: 16 / 9 }}
|
||||||
<a href="https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=readme">
|
controls
|
||||||
<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>
|
---
|
||||||
|
|
||||||
|
## 🧩 Plugins
|
||||||
|
|
||||||
|
<a href="https://sdk.thewidlarzgroup.com/offline-video?utm_source=rnv&utm_medium=readme&utm_id=banner">
|
||||||
|
<img src="./docs/assets/baners/offline-sdk-banner.png" alt="Offline SDK Preview" width="40%" align="right" />
|
||||||
</a>
|
</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 pay‑per‑issue 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) | Plug‑and‑play 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) | Hands‑on 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 world‑class 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 we’re 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)
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ buildscript {
|
|||||||
def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['RNVideo_kotlinVersion']
|
def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['RNVideo_kotlinVersion']
|
||||||
def requiredKotlinVersion = 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 isVersionAtLeast = { version, requiredVersion ->
|
||||||
def (v1, v2) = [version, requiredVersion].collect { it.tokenize('.')*.toInteger() }
|
def (v1, v2) = [version, requiredVersion].collect { it.tokenize('.')*.toInteger() }
|
||||||
for (int i = 0; i < Math.max(v1.size(), v2.size()); i++) {
|
for (int i = 0; i < Math.max(v1.size(), v2.size()); i++) {
|
||||||
@@ -35,6 +38,11 @@ buildscript {
|
|||||||
} else {
|
} else {
|
||||||
println("Kotlin version is correct: $kotlin_version")
|
println("Kotlin version is correct: $kotlin_version")
|
||||||
}
|
}
|
||||||
|
if (!isVersionAtLeast(androidx_version, requiredAndroidxVersion)) {
|
||||||
|
throw new GradleException("AndroidX version mismatch: Project is using AndroidX version $androidx_version, but it must be at least $requiredAndroidxVersion. Please update the AndroidX version.")
|
||||||
|
} else {
|
||||||
|
println("AndroidX version is correct: $androidx_version")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,9 +99,10 @@ def configStringPath = ExoplayerDependencies
|
|||||||
.concat("buildFromSource:$media3_buildFromSource")
|
.concat("buildFromSource:$media3_buildFromSource")
|
||||||
.md5()
|
.md5()
|
||||||
|
|
||||||
if (isNewArchitectureEnabled()) {
|
// commented as new architecture not yet fully supported
|
||||||
apply plugin: "com.facebook.react"
|
// if (isNewArchitectureEnabled()) {
|
||||||
}
|
// apply plugin: "com.facebook.react"
|
||||||
|
// }
|
||||||
|
|
||||||
android {
|
android {
|
||||||
if (supportsNamespace()) {
|
if (supportsNamespace()) {
|
||||||
@@ -133,6 +142,11 @@ android {
|
|||||||
buildConfigField "boolean", "USE_EXOPLAYER_DASH", ExoplayerDependencies["useExoplayerDash"].toString()
|
buildConfigField "boolean", "USE_EXOPLAYER_DASH", ExoplayerDependencies["useExoplayerDash"].toString()
|
||||||
buildConfigField "boolean", "USE_EXOPLAYER_HLS", ExoplayerDependencies["useExoplayerHls"].toString()
|
buildConfigField "boolean", "USE_EXOPLAYER_HLS", ExoplayerDependencies["useExoplayerHls"].toString()
|
||||||
buildConfigField "boolean", "USE_EXOPLAYER_RTSP", ExoplayerDependencies["useExoplayerRtsp"].toString()
|
buildConfigField "boolean", "USE_EXOPLAYER_RTSP", ExoplayerDependencies["useExoplayerRtsp"].toString()
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters(*reactNativeArchitectures())
|
abiFilters(*reactNativeArchitectures())
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
RNVideo_kotlinVersion=1.8.0
|
RNVideo_kotlinVersion=1.8.0
|
||||||
RNVideo_minSdkVersion=23
|
RNVideo_minSdkVersion=24
|
||||||
RNVideo_targetSdkVersion=34
|
RNVideo_targetSdkVersion=35
|
||||||
RNVideo_compileSdkVersion=34
|
RNVideo_compileSdkVersion=35
|
||||||
RNVideo_ndkversion=26.1.10909125
|
RNVideo_ndkversion=27.1.12297006
|
||||||
RNVideo_buildToolsVersion=34.0.0
|
RNVideo_buildToolsVersion=35.0.0
|
||||||
RNVideo_media3Version=1.4.1
|
RNVideo_media3Version=1.8.0
|
||||||
RNVideo_useExoplayerIMA=false
|
RNVideo_useExoplayerIMA=false
|
||||||
RNVideo_useExoplayerRtsp=false
|
RNVideo_useExoplayerRtsp=false
|
||||||
RNVideo_useExoplayerSmoothStreaming=true
|
RNVideo_useExoplayerSmoothStreaming=true
|
||||||
RNVideo_useExoplayerDash=true
|
RNVideo_useExoplayerDash=true
|
||||||
RNVideo_useExoplayerHls=true
|
RNVideo_useExoplayerHls=true
|
||||||
RNVideo_androidxCoreVersion=1.13.1
|
RNVideo_androidxCoreVersion=1.13.1
|
||||||
RNVideo_androidxActivityVersion=1.8.2
|
RNVideo_androidxActivityVersion=1.9.3
|
||||||
RNVideo_buildFromMedia3Source=false
|
RNVideo_buildFromMedia3Source=false
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package androidx.media3.exoplayer.ima;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.Player;
|
||||||
|
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
|
||||||
|
import androidx.media3.exoplayer.source.MediaSource;
|
||||||
|
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
|
||||||
|
|
||||||
|
public class ImaServerSideAdInsertionMediaSource {
|
||||||
|
|
||||||
|
public static class AdsLoader {
|
||||||
|
public void setPlayer(@Nullable Player player) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void release() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
public Builder(Context context, View playerView) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setAdEventListener(Object listener) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder setAdErrorListener(Object listener) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdsLoader build() {
|
||||||
|
return new AdsLoader();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Factory implements MediaSource.Factory {
|
||||||
|
public Factory(AdsLoader adsLoader, MediaSource.Factory mediaSourceFactory) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MediaSource.Factory setDrmSessionManagerProvider(DrmSessionManagerProvider drmSessionManagerProvider) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MediaSource.Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] getSupportedTypes() {
|
||||||
|
return new int[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MediaSource createMediaSource(MediaItem mediaItem) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package androidx.media3.exoplayer.ima;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
public class ImaServerSideAdInsertionUriBuilder {
|
||||||
|
public ImaServerSideAdInsertionUriBuilder setAssetKey(String assetKey) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImaServerSideAdInsertionUriBuilder setContentSourceId(String contentSourceId) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImaServerSideAdInsertionUriBuilder setVideoId(String videoId) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImaServerSideAdInsertionUriBuilder setFormat(int format) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri build() {
|
||||||
|
return Uri.EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,43 +2,100 @@ package com.brentvatne.common.api
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import com.brentvatne.common.toolbox.DebugLog
|
|
||||||
import com.brentvatne.common.toolbox.ReactBridgeUtils
|
import com.brentvatne.common.toolbox.ReactBridgeUtils
|
||||||
import com.facebook.react.bridge.ReadableMap
|
import com.facebook.react.bridge.ReadableMap
|
||||||
|
import java.util.Objects
|
||||||
|
|
||||||
class AdsProps {
|
class AdsProps {
|
||||||
|
var type: String? = null
|
||||||
|
var streamType: String? = null
|
||||||
var adTagUrl: Uri? = null
|
var adTagUrl: Uri? = null
|
||||||
var adLanguage: String? = 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 {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (other == null || other !is AdsProps) return false
|
if (other == null || other !is AdsProps) return false
|
||||||
return (
|
return (
|
||||||
adTagUrl == other.adTagUrl &&
|
type == other.type &&
|
||||||
adLanguage == other.adLanguage
|
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int =
|
||||||
|
Objects.hash(
|
||||||
|
type, streamType, adTagUrl, adLanguage, contentSourceId, videoId, assetKey, format, adTagParameters, fallbackUri
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
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_TAG_URL = "adTagUrl"
|
||||||
private const val PROP_AD_LANGUAGE = "adLanguage"
|
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
|
@JvmStatic
|
||||||
fun parse(src: ReadableMap?): AdsProps {
|
fun parse(src: ReadableMap?): AdsProps {
|
||||||
val adsProps = AdsProps()
|
val adsProps = AdsProps()
|
||||||
DebugLog.w("olivier", "uri: parse AdsProps")
|
|
||||||
|
|
||||||
if (src != null) {
|
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)
|
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)
|
adsProps.adTagUrl = Uri.parse(uriString)
|
||||||
}
|
}
|
||||||
|
|
||||||
val languageString = ReactBridgeUtils.safeGetString(src, PROP_AD_LANGUAGE)
|
val languageString = ReactBridgeUtils.safeGetString(src, PROP_AD_LANGUAGE)
|
||||||
if (!TextUtils.isEmpty(languageString)) {
|
if (!TextUtils.isEmpty(languageString)) {
|
||||||
adsProps.adLanguage = 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
|
return adsProps
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,28 @@ class BufferConfig {
|
|||||||
var maxHeapAllocationPercent = BufferConfigPropUnsetDouble
|
var maxHeapAllocationPercent = BufferConfigPropUnsetDouble
|
||||||
var minBackBufferMemoryReservePercent = BufferConfigPropUnsetDouble
|
var minBackBufferMemoryReservePercent = BufferConfigPropUnsetDouble
|
||||||
var minBufferMemoryReservePercent = BufferConfigPropUnsetDouble
|
var minBufferMemoryReservePercent = BufferConfigPropUnsetDouble
|
||||||
|
var initialBitrate = BufferConfigPropUnsetInt
|
||||||
|
|
||||||
var live: Live = Live()
|
var live: Live = Live()
|
||||||
|
|
||||||
|
/** return true if this and src are equals */
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other == null || other !is BufferConfig) return false
|
||||||
|
return (
|
||||||
|
cacheSize == other.cacheSize &&
|
||||||
|
minBufferMs == other.minBufferMs &&
|
||||||
|
maxBufferMs == other.maxBufferMs &&
|
||||||
|
bufferForPlaybackMs == other.bufferForPlaybackMs &&
|
||||||
|
bufferForPlaybackAfterRebufferMs == other.bufferForPlaybackAfterRebufferMs &&
|
||||||
|
backBufferDurationMs == other.backBufferDurationMs &&
|
||||||
|
maxHeapAllocationPercent == other.maxHeapAllocationPercent &&
|
||||||
|
minBackBufferMemoryReservePercent == other.minBackBufferMemoryReservePercent &&
|
||||||
|
minBufferMemoryReservePercent == other.minBufferMemoryReservePercent &&
|
||||||
|
initialBitrate == other.initialBitrate &&
|
||||||
|
live == other.live
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
class Live {
|
class Live {
|
||||||
var maxPlaybackSpeed: Float = BufferConfigPropUnsetDouble.toFloat()
|
var maxPlaybackSpeed: Float = BufferConfigPropUnsetDouble.toFloat()
|
||||||
var minPlaybackSpeed: Float = BufferConfigPropUnsetDouble.toFloat()
|
var minPlaybackSpeed: Float = BufferConfigPropUnsetDouble.toFloat()
|
||||||
@@ -30,12 +49,23 @@ class BufferConfig {
|
|||||||
var minOffsetMs: Long = BufferConfigPropUnsetInt.toLong()
|
var minOffsetMs: Long = BufferConfigPropUnsetInt.toLong()
|
||||||
var targetOffsetMs: Long = BufferConfigPropUnsetInt.toLong()
|
var targetOffsetMs: Long = BufferConfigPropUnsetInt.toLong()
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other == null || other !is Live) return false
|
||||||
|
return (
|
||||||
|
maxPlaybackSpeed == other.maxPlaybackSpeed &&
|
||||||
|
minPlaybackSpeed == other.minPlaybackSpeed &&
|
||||||
|
maxOffsetMs == other.maxOffsetMs &&
|
||||||
|
minOffsetMs == other.minOffsetMs &&
|
||||||
|
targetOffsetMs == other.targetOffsetMs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val PROP_BUFFER_CONFIG_LIVE_MAX_PLAYBACK_SPEED = "maxPlaybackSpeed"
|
private const val PROP_BUFFER_CONFIG_LIVE_MAX_PLAYBACK_SPEED = "maxPlaybackSpeed"
|
||||||
private val PROP_BUFFER_CONFIG_LIVE_MIN_PLAYBACK_SPEED = "minPlaybackSpeed"
|
private const val PROP_BUFFER_CONFIG_LIVE_MIN_PLAYBACK_SPEED = "minPlaybackSpeed"
|
||||||
private val PROP_BUFFER_CONFIG_LIVE_MAX_OFFSET_MS = "maxOffsetMs"
|
private const val PROP_BUFFER_CONFIG_LIVE_MAX_OFFSET_MS = "maxOffsetMs"
|
||||||
private val PROP_BUFFER_CONFIG_LIVE_MIN_OFFSET_MS = "minOffsetMs"
|
private const val PROP_BUFFER_CONFIG_LIVE_MIN_OFFSET_MS = "minOffsetMs"
|
||||||
private val PROP_BUFFER_CONFIG_LIVE_TARGET_OFFSET_MS = "targetOffsetMs"
|
private const val PROP_BUFFER_CONFIG_LIVE_TARGET_OFFSET_MS = "targetOffsetMs"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun parse(src: ReadableMap?): Live {
|
fun parse(src: ReadableMap?): Live {
|
||||||
@@ -54,16 +84,17 @@ class BufferConfig {
|
|||||||
val BufferConfigPropUnsetInt = -1
|
val BufferConfigPropUnsetInt = -1
|
||||||
val BufferConfigPropUnsetDouble = -1.0
|
val BufferConfigPropUnsetDouble = -1.0
|
||||||
|
|
||||||
private val PROP_BUFFER_CONFIG_CACHE_SIZE = "cacheSizeMB"
|
private const val PROP_BUFFER_CONFIG_CACHE_SIZE = "cacheSizeMB"
|
||||||
private val PROP_BUFFER_CONFIG_MIN_BUFFER_MS = "minBufferMs"
|
private const val PROP_BUFFER_CONFIG_MIN_BUFFER_MS = "minBufferMs"
|
||||||
private val PROP_BUFFER_CONFIG_MAX_BUFFER_MS = "maxBufferMs"
|
private const val PROP_BUFFER_CONFIG_MAX_BUFFER_MS = "maxBufferMs"
|
||||||
private val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs"
|
private const val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs"
|
||||||
private val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = "bufferForPlaybackAfterRebufferMs"
|
private const val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = "bufferForPlaybackAfterRebufferMs"
|
||||||
private val PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT = "maxHeapAllocationPercent"
|
private const val PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT = "maxHeapAllocationPercent"
|
||||||
private val PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT = "minBackBufferMemoryReservePercent"
|
private const val PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT = "minBackBufferMemoryReservePercent"
|
||||||
private val PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT = "minBufferMemoryReservePercent"
|
private const val PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT = "minBufferMemoryReservePercent"
|
||||||
private val PROP_BUFFER_CONFIG_BACK_BUFFER_DURATION_MS = "backBufferDurationMs"
|
private const val PROP_BUFFER_CONFIG_BACK_BUFFER_DURATION_MS = "backBufferDurationMs"
|
||||||
private val PROP_BUFFER_CONFIG_LIVE = "live"
|
private const val PROP_BUFFER_CONFIG_INITIAL_BITRATE = "initialBitrate"
|
||||||
|
private const val PROP_BUFFER_CONFIG_LIVE = "live"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun parse(src: ReadableMap?): BufferConfig {
|
fun parse(src: ReadableMap?): BufferConfig {
|
||||||
@@ -90,6 +121,7 @@ class BufferConfig {
|
|||||||
BufferConfigPropUnsetDouble
|
BufferConfigPropUnsetDouble
|
||||||
)
|
)
|
||||||
bufferConfig.backBufferDurationMs = safeGetInt(src, PROP_BUFFER_CONFIG_BACK_BUFFER_DURATION_MS, BufferConfigPropUnsetInt)
|
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))
|
bufferConfig.live = Live.parse(src.getMap(PROP_BUFFER_CONFIG_LIVE))
|
||||||
}
|
}
|
||||||
return bufferConfig
|
return bufferConfig
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ data class CMCDProps(
|
|||||||
val item = array.getMap(i)
|
val item = array.getMap(i)
|
||||||
val key = item?.getString("key")
|
val key = item?.getString("key")
|
||||||
val value = when (item?.getType("value")) {
|
val value = when (item?.getType("value")) {
|
||||||
ReadableType.Number -> item.getDouble("value")
|
ReadableType.Number -> item?.getDouble("value")
|
||||||
ReadableType.String -> item.getString("value")
|
ReadableType.String -> item?.getString("value")
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ class SideLoadedTextTrackList {
|
|||||||
}
|
}
|
||||||
val sideLoadedTextTrackList = SideLoadedTextTrackList()
|
val sideLoadedTextTrackList = SideLoadedTextTrackList()
|
||||||
for (i in 0 until src.size()) {
|
for (i in 0 until src.size()) {
|
||||||
val textTrack: ReadableMap = src.getMap(i)
|
val textTrack: ReadableMap? = src.getMap(i)
|
||||||
sideLoadedTextTrackList.tracks.add(SideLoadedTextTrack.parse(textTrack))
|
textTrack?.let {
|
||||||
|
sideLoadedTextTrackList.tracks.add(SideLoadedTextTrack.parse(it))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return sideLoadedTextTrackList
|
return sideLoadedTextTrackList
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.content.ContentResolver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.TextUtils
|
|
||||||
import com.brentvatne.common.api.DRMProps.Companion.parse
|
import com.brentvatne.common.api.DRMProps.Companion.parse
|
||||||
import com.brentvatne.common.toolbox.DebugLog
|
import com.brentvatne.common.toolbox.DebugLog
|
||||||
import com.brentvatne.common.toolbox.DebugLog.e
|
import com.brentvatne.common.toolbox.DebugLog.e
|
||||||
@@ -30,6 +29,12 @@ class Source {
|
|||||||
/** Parsed value of source to playback */
|
/** Parsed value of source to playback */
|
||||||
var uri: Uri? = null
|
var uri: Uri? = null
|
||||||
|
|
||||||
|
/** True if source is a local JS asset */
|
||||||
|
var isLocalAssetFile: Boolean = false
|
||||||
|
|
||||||
|
/** True if source is a local file asset://, ... */
|
||||||
|
var isAsset: Boolean = false
|
||||||
|
|
||||||
/** Start position of playback used to resume playback */
|
/** Start position of playback used to resume playback */
|
||||||
var startPositionMs: Int = -1
|
var startPositionMs: Int = -1
|
||||||
|
|
||||||
@@ -48,6 +53,9 @@ class Source {
|
|||||||
/** Metadata to display in notification */
|
/** Metadata to display in notification */
|
||||||
var metadata: Metadata? = null
|
var metadata: Metadata? = null
|
||||||
|
|
||||||
|
/** Allowed reload before failure notification */
|
||||||
|
var minLoadRetryCount = 3
|
||||||
|
|
||||||
/** http header list */
|
/** http header list */
|
||||||
val headers: MutableMap<String, String> = HashMap()
|
val headers: MutableMap<String, String> = HashMap()
|
||||||
|
|
||||||
@@ -71,12 +79,17 @@ class Source {
|
|||||||
*/
|
*/
|
||||||
var adsProps: AdsProps? = null
|
var adsProps: AdsProps? = null
|
||||||
|
|
||||||
|
/*
|
||||||
|
* buffering configuration
|
||||||
|
*/
|
||||||
|
var bufferConfig = BufferConfig()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of sideLoaded text tracks
|
* The list of sideLoaded text tracks
|
||||||
*/
|
*/
|
||||||
var sideLoadedTextTracks: SideLoadedTextTrackList? = null
|
var sideLoadedTextTracks: SideLoadedTextTrackList? = null
|
||||||
|
|
||||||
override fun hashCode(): Int = Objects.hash(uriString, uri, startPositionMs, cropStartMs, cropEndMs, extension, metadata, headers)
|
override fun hashCode(): Int = Objects.hash(uriString, uri, startPositionMs, cropStartMs, cropEndMs, extension, metadata, headers, adsProps)
|
||||||
|
|
||||||
/** return true if this and src are equals */
|
/** return true if this and src are equals */
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
@@ -91,7 +104,11 @@ class Source {
|
|||||||
contentStartTime == other.contentStartTime &&
|
contentStartTime == other.contentStartTime &&
|
||||||
cmcdProps == other.cmcdProps &&
|
cmcdProps == other.cmcdProps &&
|
||||||
sideLoadedTextTracks == other.sideLoadedTextTracks &&
|
sideLoadedTextTracks == other.sideLoadedTextTracks &&
|
||||||
adsProps == other.adsProps
|
adsProps == other.adsProps &&
|
||||||
|
minLoadRetryCount == other.minLoadRetryCount &&
|
||||||
|
isLocalAssetFile == other.isLocalAssetFile &&
|
||||||
|
isAsset == other.isAsset &&
|
||||||
|
bufferConfig == other.bufferConfig
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +164,8 @@ class Source {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "Source"
|
private const val TAG = "Source"
|
||||||
private const val PROP_SRC_URI = "uri"
|
private const val PROP_SRC_URI = "uri"
|
||||||
|
private const val PROP_SRC_IS_LOCAL_ASSET_FILE = "isLocalAssetFile"
|
||||||
|
private const val PROP_SRC_IS_ASSET = "isAsset"
|
||||||
private const val PROP_SRC_START_POSITION = "startPosition"
|
private const val PROP_SRC_START_POSITION = "startPosition"
|
||||||
private const val PROP_SRC_CROP_START = "cropStart"
|
private const val PROP_SRC_CROP_START = "cropStart"
|
||||||
private const val PROP_SRC_CROP_END = "cropEnd"
|
private const val PROP_SRC_CROP_END = "cropEnd"
|
||||||
@@ -159,6 +178,8 @@ class Source {
|
|||||||
private const val PROP_SRC_ADS = "ad"
|
private const val PROP_SRC_ADS = "ad"
|
||||||
private const val PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION = "textTracksAllowChunklessPreparation"
|
private const val PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION = "textTracksAllowChunklessPreparation"
|
||||||
private const val PROP_SRC_TEXT_TRACKS = "textTracks"
|
private const val PROP_SRC_TEXT_TRACKS = "textTracks"
|
||||||
|
private const val PROP_SRC_MIN_LOAD_RETRY_COUNT = "minLoadRetryCount"
|
||||||
|
private const val PROP_SRC_BUFFER_CONFIG = "bufferConfig"
|
||||||
|
|
||||||
@SuppressLint("DiscouragedApi")
|
@SuppressLint("DiscouragedApi")
|
||||||
private fun getUriFromAssetId(context: Context, uriString: String): Uri? {
|
private fun getUriFromAssetId(context: Context, uriString: String): Uri? {
|
||||||
@@ -190,55 +211,53 @@ class Source {
|
|||||||
fun parse(src: ReadableMap?, context: Context): Source {
|
fun parse(src: ReadableMap?, context: Context): Source {
|
||||||
val source = Source()
|
val source = Source()
|
||||||
|
|
||||||
if (src != null) {
|
if (src == null) return source
|
||||||
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.startPositionMs = safeGetInt(src, PROP_SRC_START_POSITION, -1)
|
|
||||||
source.cropStartMs = safeGetInt(src, PROP_SRC_CROP_START, -1)
|
|
||||||
source.cropEndMs = safeGetInt(src, PROP_SRC_CROP_END, -1)
|
|
||||||
source.contentStartTime = safeGetInt(src, PROP_SRC_CONTENT_START_TIME, -1)
|
|
||||||
source.extension = safeGetString(src, PROP_SRC_TYPE, null)
|
|
||||||
source.drmProps = parse(safeGetMap(src, PROP_SRC_DRM))
|
|
||||||
source.cmcdProps = CMCDProps.parse(safeGetMap(src, PROP_SRC_CMCD))
|
|
||||||
if (BuildConfig.USE_EXOPLAYER_IMA) {
|
|
||||||
source.adsProps = AdsProps.parse(safeGetMap(src, PROP_SRC_ADS))
|
|
||||||
}
|
|
||||||
source.textTracksAllowChunklessPreparation = safeGetBool(src, PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION, true)
|
|
||||||
source.sideLoadedTextTracks = SideLoadedTextTrackList.parse(safeGetArray(src, PROP_SRC_TEXT_TRACKS))
|
|
||||||
|
|
||||||
val propSrcHeadersArray = safeGetArray(src, PROP_SRC_HEADERS)
|
safeGetString(src, PROP_SRC_URI, null)
|
||||||
if (propSrcHeadersArray != null) {
|
?.takeIf { it.isNotBlank() }
|
||||||
if (propSrcHeadersArray.size() > 0) {
|
?.let { uriString ->
|
||||||
for (i in 0 until propSrcHeadersArray.size()) {
|
var uri = Uri.parse(uriString)
|
||||||
val current = propSrcHeadersArray.getMap(i)
|
|
||||||
val key = if (current.hasKey("key")) current.getString("key") else null
|
if (!isValidScheme(uri.scheme)) {
|
||||||
val value = if (current.hasKey("value")) current.getString("value") else null
|
uri = getUriFromAssetId(context, uriString) ?: return source
|
||||||
if (key != null && value != null) {
|
}
|
||||||
source.headers[key] = value
|
|
||||||
}
|
source.uriString = uriString
|
||||||
|
source.uri = uri
|
||||||
|
}
|
||||||
|
|
||||||
|
source.isLocalAssetFile = safeGetBool(src, PROP_SRC_IS_LOCAL_ASSET_FILE, false)
|
||||||
|
source.isAsset = safeGetBool(src, PROP_SRC_IS_ASSET, false)
|
||||||
|
source.startPositionMs = safeGetInt(src, PROP_SRC_START_POSITION, -1)
|
||||||
|
source.cropStartMs = safeGetInt(src, PROP_SRC_CROP_START, -1)
|
||||||
|
source.cropEndMs = safeGetInt(src, PROP_SRC_CROP_END, -1)
|
||||||
|
source.contentStartTime = safeGetInt(src, PROP_SRC_CONTENT_START_TIME, -1)
|
||||||
|
source.extension = safeGetString(src, PROP_SRC_TYPE, null)
|
||||||
|
source.drmProps = parse(safeGetMap(src, PROP_SRC_DRM))
|
||||||
|
source.cmcdProps = CMCDProps.parse(safeGetMap(src, PROP_SRC_CMCD))
|
||||||
|
if (BuildConfig.USE_EXOPLAYER_IMA) {
|
||||||
|
source.adsProps = AdsProps.parse(safeGetMap(src, PROP_SRC_ADS))
|
||||||
|
}
|
||||||
|
source.textTracksAllowChunklessPreparation = safeGetBool(src, PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION, true)
|
||||||
|
source.sideLoadedTextTracks = SideLoadedTextTrackList.parse(safeGetArray(src, PROP_SRC_TEXT_TRACKS))
|
||||||
|
source.minLoadRetryCount = safeGetInt(src, PROP_SRC_MIN_LOAD_RETRY_COUNT, 3)
|
||||||
|
source.bufferConfig = BufferConfig.parse(safeGetMap(src, PROP_SRC_BUFFER_CONFIG))
|
||||||
|
|
||||||
|
val propSrcHeadersArray = safeGetArray(src, PROP_SRC_HEADERS)
|
||||||
|
if (propSrcHeadersArray != null) {
|
||||||
|
if (propSrcHeadersArray.size() > 0) {
|
||||||
|
for (i in 0 until propSrcHeadersArray.size()) {
|
||||||
|
val current = propSrcHeadersArray.getMap(i)
|
||||||
|
val key = current?.getString("key")
|
||||||
|
val value = current?.getString("value")
|
||||||
|
if (key != null && value != null) {
|
||||||
|
source.headers[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
source.metadata = Metadata.parse(safeGetMap(src, PROP_SRC_METADATA))
|
|
||||||
}
|
}
|
||||||
|
source.metadata = Metadata.parse(safeGetMap(src, PROP_SRC_METADATA))
|
||||||
|
|
||||||
return source
|
return source
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ enum class EventTypes(val eventName: String) {
|
|||||||
|
|
||||||
EVENT_TEXT_TRACK_DATA_CHANGED("onTextTrackDataChanged"),
|
EVENT_TEXT_TRACK_DATA_CHANGED("onTextTrackDataChanged"),
|
||||||
EVENT_VIDEO_TRACKS("onVideoTracks"),
|
EVENT_VIDEO_TRACKS("onVideoTracks"),
|
||||||
EVENT_ON_RECEIVE_AD_EVENT("onReceiveAdEvent");
|
EVENT_ON_RECEIVE_AD_EVENT("onReceiveAdEvent"),
|
||||||
|
EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED("onPictureInPictureStatusChanged");
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun toMap() =
|
fun toMap() =
|
||||||
@@ -72,7 +73,7 @@ class VideoEventEmitter {
|
|||||||
lateinit var onVideoBandwidthUpdate: (bitRateEstimate: Long, height: Int, width: Int, trackId: String?) -> Unit
|
lateinit var onVideoBandwidthUpdate: (bitRateEstimate: Long, height: Int, width: Int, trackId: String?) -> Unit
|
||||||
lateinit var onVideoPlaybackStateChanged: (isPlaying: Boolean, isSeeking: Boolean) -> Unit
|
lateinit var onVideoPlaybackStateChanged: (isPlaying: Boolean, isSeeking: Boolean) -> Unit
|
||||||
lateinit var onVideoSeek: (currentPosition: Long, seekTime: Long) -> Unit
|
lateinit var onVideoSeek: (currentPosition: Long, seekTime: Long) -> Unit
|
||||||
lateinit var onVideoSeekComplete: (currentPosition: Long) -> Unit
|
lateinit var onVideoSeekComplete: (currentPosition: Long, seekTime: Long) -> Unit
|
||||||
lateinit var onVideoEnd: () -> Unit
|
lateinit var onVideoEnd: () -> Unit
|
||||||
lateinit var onVideoFullscreenPlayerWillPresent: () -> Unit
|
lateinit var onVideoFullscreenPlayerWillPresent: () -> Unit
|
||||||
lateinit var onVideoFullscreenPlayerDidPresent: () -> Unit
|
lateinit var onVideoFullscreenPlayerDidPresent: () -> Unit
|
||||||
@@ -92,6 +93,7 @@ class VideoEventEmitter {
|
|||||||
lateinit var onVideoTracks: (videoTracks: ArrayList<VideoTrack>?) -> Unit
|
lateinit var onVideoTracks: (videoTracks: ArrayList<VideoTrack>?) -> Unit
|
||||||
lateinit var onTextTrackDataChanged: (textTrackData: String) -> Unit
|
lateinit var onTextTrackDataChanged: (textTrackData: String) -> Unit
|
||||||
lateinit var onReceiveAdEvent: (adEvent: String, adData: Map<String?, String?>?) -> Unit
|
lateinit var onReceiveAdEvent: (adEvent: String, adData: Map<String?, String?>?) -> Unit
|
||||||
|
lateinit var onPictureInPictureStatusChanged: (isActive: Boolean) -> Unit
|
||||||
|
|
||||||
fun addEventEmitters(reactContext: ThemedReactContext, view: ReactExoplayerView) {
|
fun addEventEmitters(reactContext: ThemedReactContext, view: ReactExoplayerView) {
|
||||||
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
|
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
|
||||||
@@ -140,6 +142,30 @@ class VideoEventEmitter {
|
|||||||
putString("errorException", exception.toString())
|
putString("errorException", exception.toString())
|
||||||
putString("errorCode", errorCode)
|
putString("errorCode", errorCode)
|
||||||
putString("errorStackTrace", stackTrace)
|
putString("errorStackTrace", stackTrace)
|
||||||
|
|
||||||
|
// https://github.com/facebook/react-native/blob/v0.80.2/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp#L465
|
||||||
|
putMap(
|
||||||
|
"cause",
|
||||||
|
Arguments.createMap().apply {
|
||||||
|
putString("name", exception.javaClass.simpleName)
|
||||||
|
exception.message?.let { putString("message", it) }
|
||||||
|
putArray(
|
||||||
|
"stackElements",
|
||||||
|
Arguments.createArray().apply {
|
||||||
|
exception.stackTrace.forEach { element ->
|
||||||
|
pushMap(
|
||||||
|
Arguments.createMap().apply {
|
||||||
|
putString("className", element.className)
|
||||||
|
putString("fileName", element.fileName)
|
||||||
|
putInt("lineNumber", element.lineNumber)
|
||||||
|
putString("methodName", element.methodName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -176,9 +202,11 @@ class VideoEventEmitter {
|
|||||||
putDouble("seekTime", seekTime / 1000.0)
|
putDouble("seekTime", seekTime / 1000.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onVideoSeekComplete = { currentPosition ->
|
onVideoSeekComplete = { currentPosition, seekTime ->
|
||||||
event.dispatch(EventTypes.EVENT_SEEK_COMPLETE) {
|
event.dispatch(EventTypes.EVENT_SEEK_COMPLETE) {
|
||||||
putDouble("currentTime", currentPosition / 1000.0)
|
putDouble("currentTime", currentPosition / 1000.0)
|
||||||
|
putDouble("seekTime", seekTime / 1000.0)
|
||||||
|
putInt("target", view.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onVideoEnd = {
|
onVideoEnd = {
|
||||||
@@ -285,15 +313,25 @@ class VideoEventEmitter {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onPictureInPictureStatusChanged = { isActive ->
|
||||||
|
event.dispatch(EventTypes.EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED) {
|
||||||
|
putBoolean("isActive", isActive)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class VideoCustomEvent(surfaceId: Int, viewId: Int, private val event: EventTypes, private val paramsSetter: (WritableMap.() -> Unit)?) :
|
||||||
|
Event<VideoCustomEvent>(surfaceId, viewId) {
|
||||||
|
|
||||||
|
override fun getEventName(): String = "top${event.eventName.removePrefix("on")}"
|
||||||
|
|
||||||
|
override fun getEventData(): WritableMap? = Arguments.createMap().apply(paramsSetter ?: {})
|
||||||
|
}
|
||||||
|
|
||||||
private class EventBuilder(private val surfaceId: Int, private val viewId: Int, private val dispatcher: EventDispatcher) {
|
private class EventBuilder(private val surfaceId: Int, private val viewId: Int, private val dispatcher: EventDispatcher) {
|
||||||
fun dispatch(event: EventTypes, paramsSetter: (WritableMap.() -> Unit)? = null) =
|
fun dispatch(event: EventTypes, paramsSetter: (WritableMap.() -> Unit)? = null) =
|
||||||
dispatcher.dispatchEvent(object : Event<Event<*>>(surfaceId, viewId) {
|
dispatcher.dispatchEvent(VideoCustomEvent(surfaceId, viewId, event, paramsSetter))
|
||||||
override fun getEventName() = "top${event.eventName.removePrefix("on")}"
|
|
||||||
override fun getEventData() = Arguments.createMap().apply(paramsSetter ?: {})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun audioTracksToArray(audioTracks: java.util.ArrayList<Track>?): WritableArray =
|
private fun audioTracksToArray(audioTracks: java.util.ArrayList<Track>?): WritableArray =
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.brentvatne.exoplayer
|
|||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.exoplayer.upstream.CmcdConfiguration
|
import androidx.media3.exoplayer.upstream.CmcdConfiguration
|
||||||
import com.brentvatne.common.api.CMCDProps
|
import com.brentvatne.common.api.CMCDProps
|
||||||
|
import com.brentvatne.common.toolbox.DebugLog
|
||||||
import com.google.common.collect.ImmutableListMultimap
|
import com.google.common.collect.ImmutableListMultimap
|
||||||
|
|
||||||
class CMCDConfig(private val props: CMCDProps) {
|
class CMCDConfig(private val props: CMCDProps) {
|
||||||
@@ -15,9 +16,21 @@ class CMCDConfig(private val props: CMCDProps) {
|
|||||||
object : CmcdConfiguration.RequestConfig {
|
object : CmcdConfiguration.RequestConfig {
|
||||||
override fun getCustomData(): ImmutableListMultimap<String, String> = buildCustomData()
|
override fun getCustomData(): ImmutableListMultimap<String, String> = buildCustomData()
|
||||||
},
|
},
|
||||||
props.mode
|
intToCmcdMode(props.mode)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun intToCmcdMode(mode: Int): Int =
|
||||||
|
when (mode) {
|
||||||
|
0 -> CmcdConfiguration.MODE_REQUEST_HEADER
|
||||||
|
|
||||||
|
1 -> CmcdConfiguration.MODE_QUERY_PARAMETER
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
DebugLog.e("CMCDConfig", "Unsupported mode: $mode, fallback on MODE_REQUEST_HEADER")
|
||||||
|
CmcdConfiguration.MODE_REQUEST_HEADER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildCustomData(): ImmutableListMultimap<String, String> =
|
private fun buildCustomData(): ImmutableListMultimap<String, String> =
|
||||||
ImmutableListMultimap.builder<String, String>().apply {
|
ImmutableListMultimap.builder<String, String>().apply {
|
||||||
addFormattedData(this, CmcdConfiguration.KEY_CMCD_OBJECT, props.cmcdObject)
|
addFormattedData(this, CmcdConfiguration.KEY_CMCD_OBJECT, props.cmcdObject)
|
||||||
|
|||||||
59
android/src/main/java/com/brentvatne/exoplayer/DRMManager.kt
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package com.brentvatne.exoplayer
|
||||||
|
|
||||||
|
import androidx.media3.common.util.Util
|
||||||
|
import androidx.media3.datasource.HttpDataSource
|
||||||
|
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager
|
||||||
|
import androidx.media3.exoplayer.drm.DrmSessionManager
|
||||||
|
import androidx.media3.exoplayer.drm.FrameworkMediaDrm
|
||||||
|
import androidx.media3.exoplayer.drm.HttpMediaDrmCallback
|
||||||
|
import androidx.media3.exoplayer.drm.UnsupportedDrmException
|
||||||
|
import com.brentvatne.common.api.DRMProps
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class DRMManager(private val dataSourceFactory: HttpDataSource.Factory) : DRMManagerSpec {
|
||||||
|
private var hasDrmFailed = false
|
||||||
|
|
||||||
|
@Throws(UnsupportedDrmException::class)
|
||||||
|
override fun buildDrmSessionManager(uuid: UUID, drmProps: DRMProps): DrmSessionManager? = buildDrmSessionManager(uuid, drmProps, 0)
|
||||||
|
|
||||||
|
@Throws(UnsupportedDrmException::class)
|
||||||
|
private fun buildDrmSessionManager(uuid: UUID, drmProps: DRMProps, retryCount: Int = 0): DrmSessionManager? {
|
||||||
|
if (Util.SDK_INT < 18) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val drmCallback = HttpMediaDrmCallback(drmProps.drmLicenseServer, dataSourceFactory)
|
||||||
|
|
||||||
|
// Set DRM headers
|
||||||
|
val keyRequestPropertiesArray = drmProps.drmLicenseHeader
|
||||||
|
for (i in keyRequestPropertiesArray.indices step 2) {
|
||||||
|
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
val mediaDrm = FrameworkMediaDrm.newInstance(uuid)
|
||||||
|
|
||||||
|
// TODO: This isn't very secure, should be fixed
|
||||||
|
if (hasDrmFailed) {
|
||||||
|
// When DRM fails using L1 we want to switch to L3
|
||||||
|
mediaDrm.setPropertyString("securityLevel", "L3")
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultDrmSessionManager.Builder()
|
||||||
|
.setUuidAndExoMediaDrmProvider(uuid) { mediaDrm }
|
||||||
|
.setKeyRequestParameters(null)
|
||||||
|
.setMultiSession(drmProps.multiDrm)
|
||||||
|
.build(drmCallback)
|
||||||
|
} catch (ex: UnsupportedDrmException) {
|
||||||
|
hasDrmFailed = true
|
||||||
|
throw ex
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
if (retryCount < 3) {
|
||||||
|
// Attempt retry 3 times in case where the OS Media DRM Framework fails for whatever reason
|
||||||
|
hasDrmFailed = true
|
||||||
|
return buildDrmSessionManager(uuid, drmProps, retryCount + 1)
|
||||||
|
}
|
||||||
|
throw UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.brentvatne.exoplayer
|
||||||
|
|
||||||
|
import androidx.media3.exoplayer.drm.DrmSessionManager
|
||||||
|
import androidx.media3.exoplayer.drm.UnsupportedDrmException
|
||||||
|
import com.brentvatne.common.api.DRMProps
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
interface DRMManagerSpec {
|
||||||
|
/**
|
||||||
|
* Build a DRM session manager for the given UUID and DRM properties
|
||||||
|
* @param uuid The DRM system UUID
|
||||||
|
* @param drmProps The DRM properties from the source
|
||||||
|
* @return DrmSessionManager instance or null if not supported
|
||||||
|
* @throws UnsupportedDrmException if the DRM scheme is not supported
|
||||||
|
*/
|
||||||
|
@Throws(UnsupportedDrmException::class)
|
||||||
|
fun buildDrmSessionManager(uuid: UUID, drmProps: DRMProps): DrmSessionManager?
|
||||||
|
}
|
||||||
@@ -5,13 +5,26 @@ import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter
|
|||||||
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy
|
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy
|
||||||
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
|
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
|
||||||
|
|
||||||
class DefaultReactExoplayerConfig(context: Context) : ReactExoplayerConfig {
|
class DefaultReactExoplayerConfig(private val context: Context, override var initialBitrate: Long? = null) : ReactExoplayerConfig {
|
||||||
|
|
||||||
|
private var bandWidthMeter: DefaultBandwidthMeter = createBandwidthMeter(initialBitrate)
|
||||||
|
|
||||||
private var bandWidthMeter: DefaultBandwidthMeter = DefaultBandwidthMeter.Builder(context).build()
|
|
||||||
override var disableDisconnectError: Boolean = false
|
override var disableDisconnectError: Boolean = false
|
||||||
|
|
||||||
override val bandwidthMeter: DefaultBandwidthMeter
|
override val bandwidthMeter: DefaultBandwidthMeter
|
||||||
get() = bandWidthMeter
|
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 =
|
override fun buildLoadErrorHandlingPolicy(minLoadRetryCount: Int): LoadErrorHandlingPolicy =
|
||||||
if (disableDisconnectError) {
|
if (disableDisconnectError) {
|
||||||
ReactExoplayerLoadErrorHandlingPolicy(minLoadRetryCount)
|
ReactExoplayerLoadErrorHandlingPolicy(minLoadRetryCount)
|
||||||
|
|||||||
@@ -1,279 +1,269 @@
|
|||||||
package com.brentvatne.exoplayer
|
package com.brentvatne.exoplayer
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.graphics.Color
|
||||||
import android.util.TypedValue
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.view.Gravity
|
import android.util.AttributeSet
|
||||||
import android.view.SurfaceView
|
|
||||||
import android.view.TextureView
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.View.MeasureSpec
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.core.content.ContextCompat
|
import android.widget.TextView
|
||||||
import androidx.media3.common.AdViewProvider
|
|
||||||
import androidx.media3.common.C
|
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.Tracks
|
import androidx.media3.common.Timeline
|
||||||
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.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.ui.SubtitleView
|
import androidx.media3.ui.AspectRatioFrameLayout
|
||||||
|
import androidx.media3.ui.DefaultTimeBar
|
||||||
|
import androidx.media3.ui.PlayerView
|
||||||
import com.brentvatne.common.api.ResizeMode
|
import com.brentvatne.common.api.ResizeMode
|
||||||
import com.brentvatne.common.api.SubtitleStyle
|
import com.brentvatne.common.api.SubtitleStyle
|
||||||
import com.brentvatne.common.api.ViewType
|
|
||||||
import com.brentvatne.common.toolbox.DebugLog
|
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class ExoPlayerView(private val context: Context) :
|
class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
|
||||||
FrameLayout(context, null, 0),
|
FrameLayout(context, attrs, defStyleAttr) {
|
||||||
AdViewProvider {
|
|
||||||
|
|
||||||
private var surfaceView: View? = null
|
|
||||||
private var shutterView: View
|
|
||||||
private var subtitleLayout: SubtitleView
|
|
||||||
private var layout: AspectRatioFrameLayout
|
|
||||||
private var componentListener: ComponentListener
|
|
||||||
private var player: ExoPlayer? = null
|
|
||||||
private var layoutParams: ViewGroup.LayoutParams = ViewGroup.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
)
|
|
||||||
private var adOverlayFrameLayout: FrameLayout
|
|
||||||
val isPlaying: Boolean
|
|
||||||
get() = player != null && player?.isPlaying == true
|
|
||||||
|
|
||||||
@ViewType.ViewType
|
|
||||||
private var viewType = ViewType.VIEW_TYPE_SURFACE
|
|
||||||
private var hideShutterView = false
|
|
||||||
|
|
||||||
private var localStyle = SubtitleStyle()
|
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 {
|
init {
|
||||||
componentListener = ComponentListener()
|
// Add PlayerView with explicit layout parameters
|
||||||
|
val playerViewLayoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||||
|
addView(playerView, playerViewLayoutParams)
|
||||||
|
|
||||||
val aspectRatioParams = LayoutParams(
|
// Add live badge with its own layout parameters
|
||||||
LayoutParams.MATCH_PARENT,
|
val liveBadgeLayoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
||||||
LayoutParams.MATCH_PARENT
|
liveBadgeLayoutParams.setMargins(16, 16, 16, 16)
|
||||||
)
|
addView(liveBadge, liveBadgeLayoutParams)
|
||||||
aspectRatioParams.gravity = Gravity.CENTER
|
|
||||||
layout = AspectRatioFrameLayout(context)
|
|
||||||
layout.layoutParams = aspectRatioParams
|
|
||||||
|
|
||||||
shutterView = View(context)
|
|
||||||
shutterView.layoutParams = layoutParams
|
|
||||||
shutterView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.black))
|
|
||||||
|
|
||||||
subtitleLayout = SubtitleView(context)
|
|
||||||
subtitleLayout.layoutParams = layoutParams
|
|
||||||
subtitleLayout.setUserDefaultStyle()
|
|
||||||
subtitleLayout.setUserDefaultTextSize()
|
|
||||||
|
|
||||||
updateSurfaceView(viewType)
|
|
||||||
|
|
||||||
adOverlayFrameLayout = FrameLayout(context)
|
|
||||||
|
|
||||||
layout.addView(shutterView, 1, layoutParams)
|
|
||||||
if (localStyle.subtitlesFollowVideo) {
|
|
||||||
layout.addView(subtitleLayout, layoutParams)
|
|
||||||
layout.addView(adOverlayFrameLayout, layoutParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
addViewInLayout(layout, 0, aspectRatioParams)
|
|
||||||
if (!localStyle.subtitlesFollowVideo) {
|
|
||||||
addViewInLayout(subtitleLayout, 1, layoutParams)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearVideoView() {
|
fun setPlayer(player: ExoPlayer?) {
|
||||||
when (val view = surfaceView) {
|
val currentPlayer = playerView.player
|
||||||
is TextureView -> player?.clearVideoTextureView(view)
|
|
||||||
|
|
||||||
is SurfaceView -> player?.clearVideoSurfaceView(view)
|
if (currentPlayer != null) {
|
||||||
|
currentPlayer.removeListener(playerListener)
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
playerView.player = player
|
||||||
Log.w(
|
|
||||||
"clearVideoView",
|
if (player != null) {
|
||||||
"Unexpected surfaceView type: ${surfaceView?.javaClass?.name}"
|
player.addListener(playerListener)
|
||||||
)
|
|
||||||
|
// Apply pending resize mode if we have one
|
||||||
|
pendingResizeMode?.let { resizeMode ->
|
||||||
|
playerView.resizeMode = resizeMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setVideoView() {
|
fun getPlayerView(): PlayerView = playerView
|
||||||
when (val view = surfaceView) {
|
|
||||||
is TextureView -> player?.setVideoTextureView(view)
|
|
||||||
|
|
||||||
is SurfaceView -> player?.setVideoSurfaceView(view)
|
fun setResizeMode(@ResizeMode.Mode resizeMode: Int) {
|
||||||
|
val targetResizeMode = when (resizeMode) {
|
||||||
else -> {
|
ResizeMode.RESIZE_MODE_FILL -> AspectRatioFrameLayout.RESIZE_MODE_FILL
|
||||||
Log.w(
|
ResizeMode.RESIZE_MODE_CENTER_CROP -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
||||||
"setVideoView",
|
ResizeMode.RESIZE_MODE_FIT -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||||
"Unexpected surfaceView type: ${surfaceView?.javaClass?.name}"
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
fun setSubtitleStyle(style: SubtitleStyle) {
|
||||||
// ensure we reset subtitle style before reapplying it
|
playerView.subtitleView?.let { subtitleView ->
|
||||||
subtitleLayout.setUserDefaultStyle()
|
// Reset to defaults
|
||||||
subtitleLayout.setUserDefaultTextSize()
|
subtitleView.setUserDefaultStyle()
|
||||||
|
subtitleView.setUserDefaultTextSize()
|
||||||
|
|
||||||
if (style.fontSize > 0) {
|
// Apply custom styling
|
||||||
subtitleLayout.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, style.fontSize.toFloat())
|
if (style.fontSize > 0) {
|
||||||
}
|
subtitleView.setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, style.fontSize.toFloat())
|
||||||
subtitleLayout.setPadding(
|
}
|
||||||
style.paddingLeft,
|
|
||||||
style.paddingTop,
|
subtitleView.setPadding(
|
||||||
style.paddingTop,
|
style.paddingLeft,
|
||||||
style.paddingBottom
|
style.paddingTop,
|
||||||
)
|
style.paddingRight,
|
||||||
if (style.opacity != 0.0f) {
|
style.paddingBottom
|
||||||
subtitleLayout.alpha = style.opacity
|
)
|
||||||
subtitleLayout.visibility = View.VISIBLE
|
|
||||||
} else {
|
if (style.opacity != 0.0f) {
|
||||||
subtitleLayout.visibility = View.GONE
|
subtitleView.alpha = style.opacity
|
||||||
}
|
subtitleView.visibility = android.view.View.VISIBLE
|
||||||
if (localStyle.subtitlesFollowVideo != style.subtitlesFollowVideo) {
|
} else {
|
||||||
// No need to manipulate layout if value didn't change
|
subtitleView.visibility = android.view.View.GONE
|
||||||
if (style.subtitlesFollowVideo) {
|
|
||||||
removeViewInLayout(subtitleLayout)
|
|
||||||
layout.addView(subtitleLayout, layoutParams)
|
|
||||||
} else {
|
|
||||||
layout.removeViewInLayout(subtitleLayout)
|
|
||||||
addViewInLayout(subtitleLayout, 1, layoutParams, false)
|
|
||||||
}
|
}
|
||||||
requestLayout()
|
|
||||||
}
|
}
|
||||||
localStyle = style
|
localStyle = style
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setShutterColor(color: Int) {
|
fun setShutterColor(color: Int) {
|
||||||
shutterView.setBackgroundColor(color)
|
playerView.setShutterBackgroundColor(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateSurfaceView(@ViewType.ViewType viewType: Int) {
|
fun updateSurfaceView(viewType: Int) {
|
||||||
this.viewType = viewType
|
// TODO: Implement proper surface type switching if needed
|
||||||
var viewNeedRefresh = false
|
}
|
||||||
when (viewType) {
|
|
||||||
ViewType.VIEW_TYPE_SURFACE, ViewType.VIEW_TYPE_SURFACE_SECURE -> {
|
val isPlaying: Boolean
|
||||||
if (surfaceView !is SurfaceView) {
|
get() = playerView.player?.isPlaying ?: false
|
||||||
surfaceView = SurfaceView(context)
|
|
||||||
viewNeedRefresh = true
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showController() {
|
||||||
|
playerView.showController()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideController() {
|
||||||
|
playerView.hideController()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setControllerShowTimeoutMs(showTimeoutMs: Int) {
|
||||||
|
playerView.controllerShowTimeoutMs = showTimeoutMs
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setControllerAutoShow(autoShow: Boolean) {
|
||||||
|
playerView.controllerAutoShow = autoShow
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setControllerHideOnTouch(hideOnTouch: Boolean) {
|
||||||
|
playerView.controllerHideOnTouch = hideOnTouch
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFullscreenButtonClickListener(listener: PlayerView.FullscreenButtonClickListener?) {
|
||||||
|
playerView.setFullscreenButtonClickListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setShowSubtitleButton(show: Boolean) {
|
||||||
|
playerView.setShowSubtitleButton(show)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isControllerVisible(): Boolean = playerView.isControllerFullyVisible
|
||||||
|
|
||||||
|
fun setControllerVisibilityListener(listener: PlayerView.ControllerVisibilityListener?) {
|
||||||
|
playerView.setControllerVisibilityListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addOnLayoutChangeListener(listener: View.OnLayoutChangeListener) {
|
||||||
|
playerView.addOnLayoutChangeListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setFocusable(focusable: Boolean) {
|
||||||
|
playerView.isFocusable = focusable
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateLiveUi() {
|
||||||
|
val player = playerView.player ?: return
|
||||||
|
val isLive = player.isCurrentMediaItemLive
|
||||||
|
val seekable = player.isCurrentMediaItemSeekable
|
||||||
|
|
||||||
|
// Show/hide badge
|
||||||
|
liveBadge.visibility = if (isLive) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
// Disable/enable scrubbing based on seekable
|
||||||
|
val timeBar = playerView.findViewById<DefaultTimeBar?>(androidx.media3.ui.R.id.exo_progress)
|
||||||
|
timeBar?.isEnabled = !isLive || seekable
|
||||||
|
}
|
||||||
|
|
||||||
|
private val playerListener = object : Player.Listener {
|
||||||
|
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
||||||
|
playerView.post {
|
||||||
|
playerView.requestLayout()
|
||||||
|
// Reapply resize mode to ensure it's properly set after timeline changes
|
||||||
|
pendingResizeMode?.let { resizeMode ->
|
||||||
|
playerView.resizeMode = resizeMode
|
||||||
}
|
}
|
||||||
(surfaceView as SurfaceView).setSecure(viewType == ViewType.VIEW_TYPE_SURFACE_SECURE)
|
}
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewType.VIEW_TYPE_TEXTURE -> {
|
// Handle video size changes which affect aspect ratio
|
||||||
if (surfaceView !is TextureView) {
|
if (events.contains(Player.EVENT_VIDEO_SIZE_CHANGED)) {
|
||||||
surfaceView = TextureView(context)
|
pendingResizeMode?.let { resizeMode ->
|
||||||
viewNeedRefresh = true
|
playerView.resizeMode = resizeMode
|
||||||
}
|
}
|
||||||
// Support opacity properly:
|
playerView.requestLayout()
|
||||||
(surfaceView as TextureView).isOpaque = false
|
requestLayout()
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
DebugLog.wtf(TAG, "Unexpected texture view type: $viewType")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewNeedRefresh) {
|
|
||||||
surfaceView?.layoutParams = layoutParams
|
|
||||||
|
|
||||||
if (layout.getChildAt(0) != null) {
|
|
||||||
layout.removeViewAt(0)
|
|
||||||
}
|
|
||||||
layout.addView(surfaceView, 0, layoutParams)
|
|
||||||
|
|
||||||
if (this.player != null) {
|
|
||||||
setVideoView()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideShutterView() {
|
companion object {
|
||||||
shutterView.setVisibility(INVISIBLE)
|
private const val TAG = "ExoPlayerView"
|
||||||
surfaceView?.setAlpha(1f)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showShutterView() {
|
|
||||||
shutterView.setVisibility(VISIBLE)
|
|
||||||
surfaceView?.setAlpha(0f)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showAds() {
|
|
||||||
adOverlayFrameLayout.setVisibility(View.VISIBLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideAds() {
|
|
||||||
adOverlayFrameLayout.setVisibility(View.GONE)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateShutterViewVisibility() {
|
|
||||||
shutterView.visibility = if (this.hideShutterView) {
|
|
||||||
View.INVISIBLE
|
|
||||||
} else {
|
|
||||||
View.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun requestLayout() {
|
|
||||||
super.requestLayout()
|
|
||||||
post(measureAndLayout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdsLoader.AdViewProvider implementation.
|
|
||||||
override fun getAdViewGroup(): ViewGroup =
|
|
||||||
Assertions.checkNotNull(
|
|
||||||
adOverlayFrameLayout,
|
|
||||||
"exo_ad_overlay must be present for ad playback"
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the {@link ExoPlayer} to use. The {@link ExoPlayer#addListener} method of the
|
|
||||||
* player will be called and previous
|
|
||||||
* assignments are overridden.
|
|
||||||
*
|
|
||||||
* @param player The {@link ExoPlayer} to use.
|
|
||||||
*/
|
|
||||||
fun setPlayer(player: ExoPlayer?) {
|
|
||||||
if (this.player == player) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (this.player != null) {
|
|
||||||
this.player!!.removeListener(componentListener)
|
|
||||||
clearVideoView()
|
|
||||||
}
|
|
||||||
this.player = player
|
|
||||||
|
|
||||||
updateShutterViewVisibility()
|
|
||||||
if (player != null) {
|
|
||||||
setVideoView()
|
|
||||||
player.addListener(componentListener)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the resize mode which can be of value {@link ResizeMode.Mode}
|
* React Native (Yoga) can sometimes defer layout passes that are required by
|
||||||
*
|
* PlayerView for its child views (controller overlay, surface view, subtitle view, …).
|
||||||
* @param resizeMode The resize mode.
|
* This helper forces a second measure / layout after RN finishes, ensuring the
|
||||||
|
* internal views receive the final size. The same approach is used in the v7
|
||||||
|
* implementation (see VideoView.kt) and in React Native core (Toolbar example [link]).
|
||||||
*/
|
*/
|
||||||
fun setResizeMode(@ResizeMode.Mode resizeMode: Int) {
|
private val layoutRunnable = Runnable {
|
||||||
if (layout.resizeMode != resizeMode) {
|
|
||||||
layout.resizeMode = resizeMode
|
|
||||||
post(measureAndLayout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setHideShutterView(hideShutterView: Boolean) {
|
|
||||||
this.hideShutterView = hideShutterView
|
|
||||||
updateShutterViewVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val measureAndLayout: Runnable = Runnable {
|
|
||||||
measure(
|
measure(
|
||||||
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
||||||
@@ -281,58 +271,19 @@ class ExoPlayerView(private val context: Context) :
|
|||||||
layout(left, top, right, bottom)
|
layout(left, top, right, bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateForCurrentTrackSelections(tracks: Tracks?) {
|
override fun requestLayout() {
|
||||||
if (tracks == null) {
|
super.requestLayout()
|
||||||
return
|
// Post a second layout pass so the ExoPlayer internal views get correct bounds.
|
||||||
}
|
post(layoutRunnable)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun invalidateAspectRatio() {
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||||
// Resetting aspect ratio will force layout refresh on next video size changed
|
super.onLayout(changed, left, top, right, bottom)
|
||||||
layout.invalidateAspectRatio()
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class ComponentListener : Player.Listener {
|
if (changed) {
|
||||||
override fun onCues(cues: List<Cue>) {
|
pendingResizeMode?.let { resizeMode ->
|
||||||
subtitleLayout.setCues(cues)
|
playerView.resizeMode = resizeMode
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ import android.app.Dialog
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
@@ -90,7 +89,6 @@ class FullScreenPlayerView(
|
|||||||
parent?.removeView(exoPlayerView)
|
parent?.removeView(exoPlayerView)
|
||||||
containerView.addView(exoPlayerView, generateDefaultLayoutParams())
|
containerView.addView(exoPlayerView, generateDefaultLayoutParams())
|
||||||
playerControlView?.let {
|
playerControlView?.let {
|
||||||
updateFullscreenButton(playerControlView, true)
|
|
||||||
parent?.removeView(it)
|
parent?.removeView(it)
|
||||||
containerView.addView(it, generateDefaultLayoutParams())
|
containerView.addView(it, generateDefaultLayoutParams())
|
||||||
}
|
}
|
||||||
@@ -103,7 +101,6 @@ class FullScreenPlayerView(
|
|||||||
containerView.removeView(exoPlayerView)
|
containerView.removeView(exoPlayerView)
|
||||||
parent?.addView(exoPlayerView, generateDefaultLayoutParams())
|
parent?.addView(exoPlayerView, generateDefaultLayoutParams())
|
||||||
playerControlView?.let {
|
playerControlView?.let {
|
||||||
updateFullscreenButton(playerControlView, false)
|
|
||||||
containerView.removeView(it)
|
containerView.removeView(it)
|
||||||
parent?.addView(it, generateDefaultLayoutParams())
|
parent?.addView(it, generateDefaultLayoutParams())
|
||||||
}
|
}
|
||||||
@@ -125,6 +122,14 @@ class FullScreenPlayerView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hideWithoutPlayer() {
|
||||||
|
for (i in 0 until containerView.childCount) {
|
||||||
|
if (containerView.getChildAt(i) !== exoPlayerView) {
|
||||||
|
containerView.getChildAt(i).visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getFullscreenIconResource(isFullscreen: Boolean): Int =
|
private fun getFullscreenIconResource(isFullscreen: Boolean): Int =
|
||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
androidx.media3.ui.R.drawable.exo_icon_fullscreen_exit
|
androidx.media3.ui.R.drawable.exo_icon_fullscreen_exit
|
||||||
@@ -132,20 +137,6 @@ class FullScreenPlayerView(
|
|||||||
androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter
|
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() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
if (reactExoplayerView.preventsDisplaySleepDuringVideoPlayback) {
|
if (reactExoplayerView.preventsDisplaySleepDuringVideoPlayback) {
|
||||||
@@ -217,13 +208,7 @@ class FullScreenPlayerView(
|
|||||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (controlsConfig.hideNotificationBarOnFullScreenMode) {
|
// Note: Live container adjustment is no longer needed since we're using PlayerView's built-in controls
|
||||||
val liveContainer = playerControlView?.findViewById<LinearLayout?>(com.brentvatne.react.R.id.exo_live_container)
|
// PlayerView handles UI adjustments automatically
|
||||||
liveContainer?.let {
|
|
||||||
val layoutParams = it.layoutParams as LinearLayout.LayoutParams
|
|
||||||
layoutParams.topMargin = 40
|
|
||||||
it.layoutParams = layoutParams
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
package com.brentvatne.exoplayer
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.AppOpsManager
|
||||||
|
import android.app.PictureInPictureParams
|
||||||
|
import android.app.RemoteAction
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Process
|
||||||
|
import android.util.Rational
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.annotation.ChecksSdkIntAtLeast
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.app.AppOpsManagerCompat
|
||||||
|
import androidx.core.app.PictureInPictureModeChangedInfo
|
||||||
|
import androidx.core.util.Consumer
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import com.brentvatne.common.toolbox.DebugLog
|
||||||
|
import com.brentvatne.receiver.PictureInPictureReceiver
|
||||||
|
import com.facebook.react.uimanager.ThemedReactContext
|
||||||
|
|
||||||
|
internal fun Context.findActivity(): ComponentActivity {
|
||||||
|
var context = this
|
||||||
|
while (context is ContextWrapper) {
|
||||||
|
if (context is ComponentActivity) return context
|
||||||
|
context = context.baseContext
|
||||||
|
}
|
||||||
|
throw IllegalStateException("Picture in picture should be called in the context of an Activity")
|
||||||
|
}
|
||||||
|
|
||||||
|
object PictureInPictureUtil {
|
||||||
|
private const val FLAG_SUPPORTS_PICTURE_IN_PICTURE = 0x400000
|
||||||
|
private const val TAG = "PictureInPictureUtil"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun addLifecycleEventListener(context: ThemedReactContext, view: ReactExoplayerView): Runnable {
|
||||||
|
val activity = context.findActivity()
|
||||||
|
|
||||||
|
val onPictureInPictureModeChanged = Consumer<PictureInPictureModeChangedInfo> { info ->
|
||||||
|
view.setIsInPictureInPicture(info.isInPictureInPictureMode)
|
||||||
|
if (!info.isInPictureInPictureMode && activity.lifecycle.currentState == Lifecycle.State.CREATED) {
|
||||||
|
// when user click close button of PIP
|
||||||
|
if (!view.playInBackground) view.setPausedModifier(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onUserLeaveHintCallback = Runnable {
|
||||||
|
if (view.enterPictureInPictureOnLeave) {
|
||||||
|
view.enterPictureInPictureMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.addOnPictureInPictureModeChangedListener(onPictureInPictureModeChanged)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||||
|
activity.addOnUserLeaveHintListener(onUserLeaveHintCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @TODO convert to lambda when ReactExoplayerView migrated
|
||||||
|
return Runnable {
|
||||||
|
with(activity) {
|
||||||
|
removeOnPictureInPictureModeChangedListener(onPictureInPictureModeChanged)
|
||||||
|
removeOnUserLeaveHintListener(onUserLeaveHintCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun enterPictureInPictureMode(context: ThemedReactContext, pictureInPictureParams: PictureInPictureParams?) {
|
||||||
|
if (!isSupportPictureInPicture(context)) return
|
||||||
|
if (isSupportPictureInPictureAction() && pictureInPictureParams != null) {
|
||||||
|
try {
|
||||||
|
context.findActivity().enterPictureInPictureMode(pictureInPictureParams)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
DebugLog.e(TAG, e.toString())
|
||||||
|
}
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
try {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
context.findActivity().enterPictureInPictureMode()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
DebugLog.e(TAG, e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun applyPlayingStatus(
|
||||||
|
context: ThemedReactContext,
|
||||||
|
pipParamsBuilder: PictureInPictureParams.Builder?,
|
||||||
|
receiver: PictureInPictureReceiver,
|
||||||
|
isPaused: Boolean
|
||||||
|
) {
|
||||||
|
if (pipParamsBuilder == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
|
val actions = getPictureInPictureActions(context, isPaused, receiver)
|
||||||
|
pipParamsBuilder.setActions(actions)
|
||||||
|
updatePictureInPictureActions(context, pipParamsBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun applyAutoEnterEnabled(context: ThemedReactContext, pipParamsBuilder: PictureInPictureParams.Builder?, autoEnterEnabled: Boolean) {
|
||||||
|
if (pipParamsBuilder == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
|
||||||
|
pipParamsBuilder.setAutoEnterEnabled(autoEnterEnabled)
|
||||||
|
updatePictureInPictureActions(context, pipParamsBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun applySourceRectHint(context: ThemedReactContext, pipParamsBuilder: PictureInPictureParams.Builder?, playerView: ExoPlayerView) {
|
||||||
|
if (pipParamsBuilder == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
|
pipParamsBuilder.setSourceRectHint(calcRectHint(playerView))
|
||||||
|
updatePictureInPictureActions(context, pipParamsBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePictureInPictureActions(context: ThemedReactContext, pipParams: PictureInPictureParams) {
|
||||||
|
if (!isSupportPictureInPictureAction()) return
|
||||||
|
if (!isSupportPictureInPicture(context)) return
|
||||||
|
try {
|
||||||
|
context.findActivity().setPictureInPictureParams(pipParams)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
DebugLog.e(TAG, e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
fun getPictureInPictureActions(context: ThemedReactContext, isPaused: Boolean, receiver: PictureInPictureReceiver): ArrayList<RemoteAction> {
|
||||||
|
val intent = receiver.getPipActionIntent(isPaused)
|
||||||
|
val resource =
|
||||||
|
if (isPaused) androidx.media3.ui.R.drawable.exo_icon_play else androidx.media3.ui.R.drawable.exo_icon_pause
|
||||||
|
val icon = Icon.createWithResource(context, resource)
|
||||||
|
val title = if (isPaused) "play" else "pause"
|
||||||
|
return arrayListOf(RemoteAction(icon, title, title, intent))
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private fun calcRectHint(playerView: ExoPlayerView): Rect {
|
||||||
|
val hint = Rect()
|
||||||
|
// Use the PlayerView itself since surfaceView is private
|
||||||
|
playerView.getGlobalVisibleRect(hint)
|
||||||
|
val location = IntArray(2)
|
||||||
|
playerView.getLocationOnScreen(location)
|
||||||
|
|
||||||
|
val height = hint.bottom - hint.top
|
||||||
|
hint.top = location[1]
|
||||||
|
hint.bottom = hint.top + height
|
||||||
|
return hint
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
fun calcPictureInPictureAspectRatio(player: ExoPlayer): Rational {
|
||||||
|
var aspectRatio = Rational(player.videoSize.width, player.videoSize.height)
|
||||||
|
// AspectRatio for the activity in picture-in-picture, must be between 2.39:1 and 1:2.39 (inclusive).
|
||||||
|
// https://developer.android.com/reference/android/app/PictureInPictureParams.Builder#setAspectRatio(android.util.Rational)
|
||||||
|
val maximumRatio = Rational(239, 100)
|
||||||
|
val minimumRatio = Rational(100, 239)
|
||||||
|
if (aspectRatio.toFloat() > maximumRatio.toFloat()) {
|
||||||
|
aspectRatio = maximumRatio
|
||||||
|
} else if (aspectRatio.toFloat() < minimumRatio.toFloat()) {
|
||||||
|
aspectRatio = minimumRatio
|
||||||
|
}
|
||||||
|
return aspectRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isSupportPictureInPicture(context: ThemedReactContext): Boolean =
|
||||||
|
checkIsApiSupport() && checkIsSystemSupportPIP(context) && checkIsUserAllowPIP(context)
|
||||||
|
|
||||||
|
private fun isSupportPictureInPictureAction(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||||
|
|
||||||
|
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.N)
|
||||||
|
private fun checkIsApiSupport(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
|
private fun checkIsSystemSupportPIP(context: ThemedReactContext): Boolean {
|
||||||
|
val activity = context.findActivity() ?: return false
|
||||||
|
|
||||||
|
val isActivitySupportPip = try {
|
||||||
|
val activityInfo = activity.packageManager.getActivityInfo(activity.componentName, PackageManager.GET_META_DATA)
|
||||||
|
// detect current activity's android:supportsPictureInPicture value defined within AndroidManifest.xml
|
||||||
|
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/pm/ActivityInfo.java;l=1090-1093;drc=7651f0a4c059a98f32b0ba30cd64500bf135385f
|
||||||
|
activityInfo.flags and FLAG_SUPPORTS_PICTURE_IN_PICTURE != 0
|
||||||
|
} catch (e: kotlin.Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
// PIP might be disabled on devices that have low RAM.
|
||||||
|
val isPipAvailable = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
|
||||||
|
|
||||||
|
return isActivitySupportPip && isPipAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkIsUserAllowPIP(context: ThemedReactContext): Boolean {
|
||||||
|
val activity = context.currentActivity ?: return false
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
val result = AppOpsManagerCompat.noteOpNoThrow(
|
||||||
|
activity,
|
||||||
|
AppOpsManager.OPSTR_PICTURE_IN_PICTURE,
|
||||||
|
Process.myUid(),
|
||||||
|
activity.packageName
|
||||||
|
)
|
||||||
|
AppOpsManager.MODE_ALLOWED == result
|
||||||
|
} else {
|
||||||
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.O && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package com.brentvatne.exoplayer
|
||||||
|
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.datasource.DataSource
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.exoplayer.drm.DrmSessionManager
|
||||||
|
import androidx.media3.exoplayer.source.MediaSource
|
||||||
|
import com.brentvatne.common.api.Source
|
||||||
|
import com.brentvatne.react.RNVPlugin
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for RNV plugins that have dependencies or logic that is specific to Exoplayer
|
||||||
|
* It extends the RNVPlugin interface
|
||||||
|
*/
|
||||||
|
interface RNVExoplayerPlugin : RNVPlugin {
|
||||||
|
/**
|
||||||
|
* Optional function that allows plugin to provide custom DRM manager
|
||||||
|
* Only one plugin can provide DRM manager at a time
|
||||||
|
* @return DRMManagerSpec implementation if plugin wants to handle DRM, null otherwise
|
||||||
|
*/
|
||||||
|
fun getDRMManager(): DRMManagerSpec? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional function that allows the plugin to override the DrmSessionManager after it has been created.
|
||||||
|
* This is called after buildDrmSessionManager and allows for final modifications to the DrmSessionManager.
|
||||||
|
* @param source The media source being initialized.
|
||||||
|
* @param drmSessionManager The current DrmSessionManager instance.
|
||||||
|
* @return A modified DrmSessionManager if override is needed, or null to use original.
|
||||||
|
*/
|
||||||
|
fun overrideDrmSessionManager(source: Source, drmSessionManager: DrmSessionManager): DrmSessionManager? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional function that allows the plugin to override the media data source factory,
|
||||||
|
* which is responsible for loading media data.
|
||||||
|
* @param source The media source being initialized.
|
||||||
|
* @param mediaDataSourceFactory The current default data source factory.
|
||||||
|
* @return A custom [DataSource.Factory] if override is needed, or null to use default.
|
||||||
|
*/
|
||||||
|
fun overrideMediaDataSourceFactory(source: Source, mediaDataSourceFactory: DataSource.Factory): DataSource.Factory? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional function that allows the plugin to override the media source factory,
|
||||||
|
* which is responsible for loading media data.
|
||||||
|
* @param source The media source being initialized.
|
||||||
|
* @param mediaSourceFactory The current media source factory.
|
||||||
|
* @param mediaDataSourceFactory The current default data source factory.
|
||||||
|
* @return A custom [MediaSource.Factory] if override is needed, or null to use default.
|
||||||
|
*/
|
||||||
|
fun overrideMediaSourceFactory(source: Source, mediaSourceFactory: MediaSource.Factory, mediaDataSourceFactory: DataSource.Factory): MediaSource.Factory? =
|
||||||
|
null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional function that allows the plugin to modify the [MediaItem.Builder]
|
||||||
|
* before the final [MediaItem] is created.
|
||||||
|
* @param source The source from which the media item is being built.
|
||||||
|
* @param mediaItemBuilder The default [MediaItem.Builder] instance.
|
||||||
|
* @return A modified builder instance if override is needed, or null to use original.
|
||||||
|
*/
|
||||||
|
fun overrideMediaItemBuilder(source: Source, mediaItemBuilder: MediaItem.Builder): MediaItem.Builder? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional function that allows the plugin to control whether caching should be disabled
|
||||||
|
* for a given video source.
|
||||||
|
* @param source The video source being loaded.
|
||||||
|
* @return true to disable caching, false to keep it enabled.
|
||||||
|
*/
|
||||||
|
fun shouldDisableCache(source: Source): Boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when a new player is created
|
||||||
|
* @param id: a random string identifying the player
|
||||||
|
* @param player: the instantiated player reference
|
||||||
|
* @note: This is helper that ensure that player is non null ExoPlayer
|
||||||
|
*/
|
||||||
|
fun onInstanceCreated(id: String, player: ExoPlayer)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when a player should be destroyed
|
||||||
|
* when this callback is called, the plugin shall free all
|
||||||
|
* resources and release all reference to Player object
|
||||||
|
* @param id: a random string identifying the player
|
||||||
|
* @param player: the player to release
|
||||||
|
* @note: This is helper that ensure that player is non null ExoPlayer
|
||||||
|
*/
|
||||||
|
fun onInstanceRemoved(id: String, player: ExoPlayer)
|
||||||
|
|
||||||
|
override fun onInstanceCreated(id: String, player: Any) {
|
||||||
|
if (player is ExoPlayer) {
|
||||||
|
onInstanceCreated(id, player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInstanceRemoved(id: String, player: Any) {
|
||||||
|
if (player is ExoPlayer) {
|
||||||
|
onInstanceRemoved(id, player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,4 +7,6 @@ interface ReactExoplayerConfig {
|
|||||||
fun buildLoadErrorHandlingPolicy(minLoadRetryCount: Int): LoadErrorHandlingPolicy
|
fun buildLoadErrorHandlingPolicy(minLoadRetryCount: Int): LoadErrorHandlingPolicy
|
||||||
var disableDisconnectError: Boolean
|
var disableDisconnectError: Boolean
|
||||||
val bandwidthMeter: DefaultBandwidthMeter
|
val bandwidthMeter: DefaultBandwidthMeter
|
||||||
|
var initialBitrate: Long?
|
||||||
|
fun setInitialBitrate(bitrate: Long)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.brentvatne.exoplayer
|
|||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.brentvatne.common.api.BufferConfig
|
|
||||||
import com.brentvatne.common.api.BufferingStrategy
|
import com.brentvatne.common.api.BufferingStrategy
|
||||||
import com.brentvatne.common.api.ControlsConfig
|
import com.brentvatne.common.api.ControlsConfig
|
||||||
import com.brentvatne.common.api.ResizeMode
|
import com.brentvatne.common.api.ResizeMode
|
||||||
@@ -33,16 +32,15 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
|||||||
private const val PROP_SELECTED_TEXT_TRACK_TYPE = "type"
|
private const val PROP_SELECTED_TEXT_TRACK_TYPE = "type"
|
||||||
private const val PROP_SELECTED_TEXT_TRACK_VALUE = "value"
|
private const val PROP_SELECTED_TEXT_TRACK_VALUE = "value"
|
||||||
private const val PROP_PAUSED = "paused"
|
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_MUTED = "muted"
|
||||||
private const val PROP_AUDIO_OUTPUT = "audioOutput"
|
private const val PROP_AUDIO_OUTPUT = "audioOutput"
|
||||||
private const val PROP_VOLUME = "volume"
|
private const val PROP_VOLUME = "volume"
|
||||||
private const val PROP_BUFFER_CONFIG = "bufferConfig"
|
|
||||||
private const val PROP_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK =
|
private const val PROP_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK =
|
||||||
"preventsDisplaySleepDuringVideoPlayback"
|
"preventsDisplaySleepDuringVideoPlayback"
|
||||||
private const val PROP_PROGRESS_UPDATE_INTERVAL = "progressUpdateInterval"
|
private const val PROP_PROGRESS_UPDATE_INTERVAL = "progressUpdateInterval"
|
||||||
private const val PROP_REPORT_BANDWIDTH = "reportBandwidth"
|
private const val PROP_REPORT_BANDWIDTH = "reportBandwidth"
|
||||||
private const val PROP_RATE = "rate"
|
private const val PROP_RATE = "rate"
|
||||||
private const val PROP_MIN_LOAD_RETRY_COUNT = "minLoadRetryCount"
|
|
||||||
private const val PROP_MAXIMUM_BIT_RATE = "maxBitRate"
|
private const val PROP_MAXIMUM_BIT_RATE = "maxBitRate"
|
||||||
private const val PROP_PLAY_IN_BACKGROUND = "playInBackground"
|
private const val PROP_PLAY_IN_BACKGROUND = "playInBackground"
|
||||||
private const val PROP_DISABLE_FOCUS = "disableFocus"
|
private const val PROP_DISABLE_FOCUS = "disableFocus"
|
||||||
@@ -54,7 +52,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
|||||||
private const val PROP_SELECTED_VIDEO_TRACK = "selectedVideoTrack"
|
private const val PROP_SELECTED_VIDEO_TRACK = "selectedVideoTrack"
|
||||||
private const val PROP_SELECTED_VIDEO_TRACK_TYPE = "type"
|
private const val PROP_SELECTED_VIDEO_TRACK_TYPE = "type"
|
||||||
private const val PROP_SELECTED_VIDEO_TRACK_VALUE = "value"
|
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_CONTROLS = "controls"
|
||||||
private const val PROP_SUBTITLE_STYLE = "subtitleStyle"
|
private const val PROP_SUBTITLE_STYLE = "subtitleStyle"
|
||||||
private const val PROP_SHUTTER_COLOR = "shutterColor"
|
private const val PROP_SHUTTER_COLOR = "shutterColor"
|
||||||
@@ -72,6 +69,7 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
|||||||
|
|
||||||
override fun onDropViewInstance(view: ReactExoplayerView) {
|
override fun onDropViewInstance(view: ReactExoplayerView) {
|
||||||
view.cleanUpResources()
|
view.cleanUpResources()
|
||||||
|
view.exitPictureInPictureMode()
|
||||||
ReactNativeVideoManager.getInstance().unregisterView(this)
|
ReactNativeVideoManager.getInstance().unregisterView(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +155,11 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
|||||||
videoView.setMutedModifier(muted)
|
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)
|
@ReactProp(name = PROP_AUDIO_OUTPUT)
|
||||||
fun setAudioOutput(videoView: ReactExoplayerView, audioOutput: String) {
|
fun setAudioOutput(videoView: ReactExoplayerView, audioOutput: String) {
|
||||||
videoView.setAudioOutput(AudioOutput.get(audioOutput))
|
videoView.setAudioOutput(AudioOutput.get(audioOutput))
|
||||||
@@ -187,11 +190,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
|||||||
videoView.setMaxBitRateModifier(maxBitRate.toInt())
|
videoView.setMaxBitRateModifier(maxBitRate.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = PROP_MIN_LOAD_RETRY_COUNT)
|
|
||||||
fun setMinLoadRetryCount(videoView: ReactExoplayerView, minLoadRetryCount: Int) {
|
|
||||||
videoView.setMinLoadRetryCountModifier(minLoadRetryCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ReactProp(name = PROP_PLAY_IN_BACKGROUND, defaultBoolean = false)
|
@ReactProp(name = PROP_PLAY_IN_BACKGROUND, defaultBoolean = false)
|
||||||
fun setPlayInBackground(videoView: ReactExoplayerView, playInBackground: Boolean) {
|
fun setPlayInBackground(videoView: ReactExoplayerView, playInBackground: Boolean) {
|
||||||
videoView.setPlayInBackground(playInBackground)
|
videoView.setPlayInBackground(playInBackground)
|
||||||
@@ -228,11 +226,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
|||||||
videoView.setViewType(viewType)
|
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)
|
@ReactProp(name = PROP_CONTROLS, defaultBoolean = false)
|
||||||
fun setControls(videoView: ReactExoplayerView, controls: Boolean) {
|
fun setControls(videoView: ReactExoplayerView, controls: Boolean) {
|
||||||
videoView.setControls(controls)
|
videoView.setControls(controls)
|
||||||
@@ -248,12 +241,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
|||||||
videoView.setShutterColor(color)
|
videoView.setShutterColor(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = PROP_BUFFER_CONFIG)
|
|
||||||
fun setBufferConfig(videoView: ReactExoplayerView, bufferConfig: ReadableMap?) {
|
|
||||||
val config = BufferConfig.parse(bufferConfig)
|
|
||||||
videoView.setBufferConfig(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ReactProp(name = PROP_SHOW_NOTIFICATION_CONTROLS)
|
@ReactProp(name = PROP_SHOW_NOTIFICATION_CONTROLS)
|
||||||
fun setShowNotificationControls(videoView: ReactExoplayerView, showNotificationControls: Boolean) {
|
fun setShowNotificationControls(videoView: ReactExoplayerView, showNotificationControls: Boolean) {
|
||||||
videoView.setShowNotificationControls(showNotificationControls)
|
videoView.setShowNotificationControls(showNotificationControls)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import androidx.media3.session.MediaSessionService
|
|||||||
import androidx.media3.session.MediaStyleNotificationHelper
|
import androidx.media3.session.MediaStyleNotificationHelper
|
||||||
import androidx.media3.session.SessionCommand
|
import androidx.media3.session.SessionCommand
|
||||||
import com.brentvatne.common.toolbox.DebugLog
|
import com.brentvatne.common.toolbox.DebugLog
|
||||||
|
import com.brentvatne.react.R
|
||||||
import okhttp3.internal.immutableListOf
|
import okhttp3.internal.immutableListOf
|
||||||
|
|
||||||
class PlaybackServiceBinder(val service: VideoPlaybackService) : Binder()
|
class PlaybackServiceBinder(val service: VideoPlaybackService) : Binder()
|
||||||
@@ -63,7 +64,9 @@ class VideoPlaybackService : MediaSessionService() {
|
|||||||
|
|
||||||
mediaSessionsList[player] = mediaSession
|
mediaSessionsList[player] = mediaSession
|
||||||
addSession(mediaSession)
|
addSession(mediaSession)
|
||||||
startForeground(mediaSession.player.hashCode(), buildNotification(mediaSession))
|
|
||||||
|
val notificationId = player.hashCode()
|
||||||
|
startForeground(notificationId, buildNotification(mediaSession))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregisterPlayer(player: ExoPlayer) {
|
fun unregisterPlayer(player: ExoPlayer) {
|
||||||
@@ -224,7 +227,30 @@ class VideoPlaybackService : MediaSessionService() {
|
|||||||
mediaSessionsList.clear()
|
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 {
|
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 {
|
intent?.let {
|
||||||
val playerId = it.getIntExtra("PLAYER_ID", -1)
|
val playerId = it.getIntExtra("PLAYER_ID", -1)
|
||||||
val actionCommand = it.getStringExtra("ACTION")
|
val actionCommand = it.getStringExtra("ACTION")
|
||||||
@@ -249,6 +275,7 @@ class VideoPlaybackService : MediaSessionService() {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val SEEK_INTERVAL_MS = 10000L
|
private const val SEEK_INTERVAL_MS = 10000L
|
||||||
private const val TAG = "VideoPlaybackService"
|
private const val TAG = "VideoPlaybackService"
|
||||||
|
private const val PLACEHOLDER_NOTIFICATION_ID = 9999
|
||||||
|
|
||||||
const val NOTIFICATION_CHANEL_ID = "RNVIDEO_SESSION_NOTIFICATION"
|
const val NOTIFICATION_CHANEL_ID = "RNVIDEO_SESSION_NOTIFICATION"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package com.brentvatne.react
|
package com.brentvatne.react
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin interface definition
|
* Plugin interface definition for RNV plugins that does not have dependencies nor logic specific to any player
|
||||||
|
* It is the base interface for all RNV plugins
|
||||||
*/
|
*/
|
||||||
interface RNVPlugin {
|
interface RNVPlugin {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
package com.brentvatne.react
|
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.common.toolbox.DebugLog
|
||||||
import com.brentvatne.exoplayer.ReactExoplayerViewManager
|
import com.brentvatne.exoplayer.DRMManagerSpec
|
||||||
|
import com.brentvatne.exoplayer.RNVExoplayerPlugin
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ReactNativeVideoManager is a singleton class which allows to manipulate / the global state of the app
|
* 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 {
|
class ReactNativeVideoManager : RNVPlugin {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -23,13 +29,14 @@ class ReactNativeVideoManager : RNVPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var instanceList: ArrayList<ReactExoplayerViewManager> = ArrayList()
|
private val pluginList = ArrayList<RNVPlugin>()
|
||||||
private var pluginList: ArrayList<RNVPlugin> = ArrayList()
|
private var customDRMManager: DRMManagerSpec? = null
|
||||||
|
private var instanceList: ArrayList<Any> = ArrayList()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* register a new ReactExoplayerViewManager in the managed list
|
* register a new ReactExoplayerViewManager in the managed list
|
||||||
*/
|
*/
|
||||||
fun registerView(newInstance: ReactExoplayerViewManager) {
|
fun registerView(newInstance: Any) {
|
||||||
if (instanceList.size > 2) {
|
if (instanceList.size > 2) {
|
||||||
DebugLog.d(TAG, "multiple Video displayed ?")
|
DebugLog.d(TAG, "multiple Video displayed ?")
|
||||||
}
|
}
|
||||||
@@ -39,7 +46,7 @@ class ReactNativeVideoManager : RNVPlugin {
|
|||||||
/**
|
/**
|
||||||
* unregister existing ReactExoplayerViewManager in the managed list
|
* unregister existing ReactExoplayerViewManager in the managed list
|
||||||
*/
|
*/
|
||||||
fun unregisterView(newInstance: ReactExoplayerViewManager) {
|
fun unregisterView(newInstance: Any) {
|
||||||
instanceList.remove(newInstance)
|
instanceList.remove(newInstance)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +55,8 @@ class ReactNativeVideoManager : RNVPlugin {
|
|||||||
*/
|
*/
|
||||||
fun registerPlugin(plugin: RNVPlugin) {
|
fun registerPlugin(plugin: RNVPlugin) {
|
||||||
pluginList.add(plugin)
|
pluginList.add(plugin)
|
||||||
return
|
|
||||||
|
maybeRegisterExoplayerPlugin(plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,9 +64,11 @@ class ReactNativeVideoManager : RNVPlugin {
|
|||||||
*/
|
*/
|
||||||
fun unregisterPlugin(plugin: RNVPlugin) {
|
fun unregisterPlugin(plugin: RNVPlugin) {
|
||||||
pluginList.remove(plugin)
|
pluginList.remove(plugin)
|
||||||
return
|
|
||||||
|
maybeUnregisterExoplayerPlugin(plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------- Generic RNV plugin methods -----------------------
|
||||||
override fun onInstanceCreated(id: String, player: Any) {
|
override fun onInstanceCreated(id: String, player: Any) {
|
||||||
pluginList.forEach { it.onInstanceCreated(id, player) }
|
pluginList.forEach { it.onInstanceCreated(id, player) }
|
||||||
}
|
}
|
||||||
@@ -66,4 +76,83 @@ class ReactNativeVideoManager : RNVPlugin {
|
|||||||
override fun onInstanceRemoved(id: String, player: Any) {
|
override fun onInstanceRemoved(id: String, player: Any) {
|
||||||
pluginList.forEach { it.onInstanceRemoved(id, player) }
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,20 @@ class VideoManagerModule(reactContext: ReactApplicationContext?) : ReactContextB
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun enterPictureInPictureCmd(reactTag: Int) {
|
||||||
|
performOnPlayerView(reactTag) {
|
||||||
|
it?.enterPictureInPictureMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun exitPictureInPictureCmd(reactTag: Int) {
|
||||||
|
performOnPlayerView(reactTag) {
|
||||||
|
it?.exitPictureInPictureMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
fun setSourceCmd(reactTag: Int, source: ReadableMap?) {
|
fun setSourceCmd(reactTag: Int, source: ReadableMap?) {
|
||||||
performOnPlayerView(reactTag) {
|
performOnPlayerView(reactTag) {
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.brentvatne.receiver
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.brentvatne.exoplayer.ReactExoplayerView
|
||||||
|
import com.facebook.react.uimanager.ThemedReactContext
|
||||||
|
|
||||||
|
class PictureInPictureReceiver(private val view: ReactExoplayerView, private val context: ThemedReactContext) : BroadcastReceiver() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ACTION_MEDIA_CONTROL = "rnv_media_control"
|
||||||
|
const val EXTRA_CONTROL_TYPE = "rnv_control_type"
|
||||||
|
|
||||||
|
// The request code for play action PendingIntent.
|
||||||
|
const val REQUEST_PLAY = 1
|
||||||
|
|
||||||
|
// The request code for pause action PendingIntent.
|
||||||
|
const val REQUEST_PAUSE = 2
|
||||||
|
|
||||||
|
// The intent extra value for play action.
|
||||||
|
const val CONTROL_TYPE_PLAY = 1
|
||||||
|
|
||||||
|
// The intent extra value for pause action.
|
||||||
|
const val CONTROL_TYPE_PAUSE = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
intent ?: return
|
||||||
|
if (intent.action == ACTION_MEDIA_CONTROL) {
|
||||||
|
when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) {
|
||||||
|
CONTROL_TYPE_PLAY -> view.setPausedModifier(false)
|
||||||
|
CONTROL_TYPE_PAUSE -> view.setPausedModifier(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setListener() {
|
||||||
|
ContextCompat.registerReceiver(context, this, IntentFilter(ACTION_MEDIA_CONTROL), ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeListener() {
|
||||||
|
try {
|
||||||
|
context.unregisterReceiver(this)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// ignore if already unregistered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPipActionIntent(isPaused: Boolean): PendingIntent {
|
||||||
|
val requestCode = if (isPaused) REQUEST_PLAY else REQUEST_PAUSE
|
||||||
|
val controlType = if (isPaused) CONTROL_TYPE_PLAY else CONTROL_TYPE_PAUSE
|
||||||
|
val flag =
|
||||||
|
if (Build.VERSION.SDK_INT >=
|
||||||
|
Build.VERSION_CODES.M
|
||||||
|
) {
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
} else {
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
}
|
||||||
|
val intent = Intent(ACTION_MEDIA_CONTROL).putExtra(
|
||||||
|
EXTRA_CONTROL_TYPE,
|
||||||
|
controlType
|
||||||
|
)
|
||||||
|
intent.setPackage(context.packageName)
|
||||||
|
return PendingIntent.getBroadcast(context, requestCode, intent, flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layoutDirection="ltr"
|
android:layoutDirection="ltr"
|
||||||
android:background="@color/midnight_black"
|
android:background="@color/player_overlay_color"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="silver_gray">#FFBEBEBE</color>
|
<color name="silver_gray">#FFBEBEBE</color>
|
||||||
<color name="midnight_black">#CC000000</color>
|
<color name="player_overlay_color">#00000000</color>
|
||||||
<color name="white">#FFFFFF</color>
|
<color name="white">#FFFFFF</color>
|
||||||
<color name="red">#FF0000</color>
|
<color name="red">#FF0000</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -22,4 +22,8 @@
|
|||||||
<string name="playback_speed">Playback Speed</string>
|
<string name="playback_speed">Playback Speed</string>
|
||||||
|
|
||||||
<string name="select_playback_speed">Select 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>
|
</resources>
|
||||||
|
|||||||
4
docs/.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
out/
|
out/
|
||||||
.next/
|
.next/
|
||||||
|
public/llms.txt
|
||||||
|
public/llms-full.txt
|
||||||
@@ -1,15 +1,3 @@
|
|||||||
# react-native-video-docs
|
# react-native-video-docs
|
||||||
|
|
||||||
This is the documentation for the [react-native-video](github.com/TheWidlarzGroup/react-native-video).
|
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.
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|||||||
BIN
docs/assets/baners/bgupload-sdk-banner.png
Normal file
|
After Width: | Height: | Size: 491 KiB |
BIN
docs/assets/baners/offline-sdk-banner.png
Normal file
|
After Width: | Height: | Size: 585 KiB |
BIN
docs/assets/baners/rnv-banner.png
Normal file
|
After Width: | Height: | Size: 565 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB |
BIN
docs/bun.lockb
@@ -3,6 +3,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spanStyle {
|
.spanStyle {
|
||||||
font-family: 'Orbitron';
|
font-family: var(--font-orbitron);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|||||||
62
docs/components/TWGBadge/TWGBadge.module.css
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
.extraContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #171717;
|
||||||
|
padding: 1rem;
|
||||||
|
gap: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extraText {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extraButton {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
background-color: #f9d85b;
|
||||||
|
transition: transform 0.3s ease, background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extraButton:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(html[class~=dark]) .extraContainer {
|
||||||
|
background-color: #87ccef;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(html[class~=dark]) .extraText {
|
||||||
|
color: #171717;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(html[class~=dark]) .extraButton {
|
||||||
|
background-color: #171717;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.visibleOnLarge {
|
||||||
|
display: inherit;
|
||||||
|
}
|
||||||
|
.visibleOnSmall {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1279px) {
|
||||||
|
.visibleOnLarge {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.visibleOnSmall {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
docs/components/TWGBadge/TWGBadge.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './TWGBadge.module.css';
|
||||||
|
|
||||||
|
interface TWGBadgeProps {
|
||||||
|
visibleOnLarge?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TWGBadge = ({visibleOnLarge}: TWGBadgeProps) => {
|
||||||
|
const visibilityClass = visibleOnLarge
|
||||||
|
? styles.visibleOnLarge
|
||||||
|
: styles.visibleOnSmall;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={[styles.extraContainer, visibilityClass].join(' ')}>
|
||||||
|
<span className={styles.extraText}>We are TheWidlarzGroup</span>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs&utm_campaign=badge&utm_id=enterprise#Contact"
|
||||||
|
className={styles.extraButton}
|
||||||
|
rel="noreferrer">
|
||||||
|
Premium support →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TWGBadge;
|
||||||
7
docs/font.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import {Orbitron} from 'next/font/google';
|
||||||
|
|
||||||
|
export const orbitron = Orbitron({
|
||||||
|
display: 'swap',
|
||||||
|
subsets: ['latin'],
|
||||||
|
weight: ['400', '900'],
|
||||||
|
});
|
||||||
56
docs/json-ld.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "TheWidlarzGroup Docs",
|
||||||
|
"url": "https://docs.thewidlarzgroup.com/react-native-video/",
|
||||||
|
"publisher": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "TheWidlarzGroup",
|
||||||
|
"url": "https://thewidlarzgroup.com",
|
||||||
|
"logo": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"url": "https://docs.thewidlarzgroup.com/react-native-video/favicon.ico"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mainEntity": {
|
||||||
|
"@type": "TechArticle",
|
||||||
|
"headline": "React Native Video Documentation",
|
||||||
|
"description": "Official documentation for React Native Video component by TheWidlarzGroup",
|
||||||
|
"hasPart": [
|
||||||
|
{
|
||||||
|
"@type": "TechArticle",
|
||||||
|
"headline": "Installation Guide",
|
||||||
|
"description": "Installation instructions for your platform to link react-native-video into your project.",
|
||||||
|
"url": "https://docs.thewidlarzgroup.com/react-native-video/installation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "TechArticle",
|
||||||
|
"headline": "Properties",
|
||||||
|
"description": "This page shows the list of available properties to configure the player.",
|
||||||
|
"url": "https://docs.thewidlarzgroup.com/react-native-video/component/props"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "TechArticle",
|
||||||
|
"headline": "DRM Implementation",
|
||||||
|
"description": "We provide a sample implementation in the example app demonstrating how to use DRM with react-native-video.",
|
||||||
|
"url": "https://docs.thewidlarzgroup.com/react-native-video/component/drm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "TechArticle",
|
||||||
|
"headline": "Downloading",
|
||||||
|
"description": "The Offline Video SDK extends react-native-video (v6 or v7) with the ability to download and store video content for offline playback.",
|
||||||
|
"url": "https://docs.thewidlarzgroup.com/react-native-video/other/downloading"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"offers": {
|
||||||
|
"@type": "Offer",
|
||||||
|
"name": "Enterprise Support",
|
||||||
|
"description": "Expert support to extend or implement React Native Video capabilities",
|
||||||
|
"url": "https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=google#Contact",
|
||||||
|
"seller": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "TheWidlarzGroup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
docs/next-env.d.ts
vendored
@@ -2,4 +2,4 @@
|
|||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"build": "bun next build"
|
"build": "bun next build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "^13.5.4",
|
"next": "14.2.20",
|
||||||
"nextra": "^2.13.2",
|
"nextra": "^2.13.2",
|
||||||
"nextra-theme-docs": "^2.13.2",
|
"nextra-theme-docs": "^2.13.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -18,4 +18,4 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bun-types": "latest"
|
"bun-types": "latest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
docs/pages/_app.mdx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import {orbitron} from '../font';
|
||||||
|
|
||||||
|
export default function Nextra({Component, pageProps}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style jsx global>{`
|
||||||
|
:root {
|
||||||
|
--font-orbitron: ${orbitron.style.fontFamily};
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,15 +7,39 @@
|
|||||||
"type": "separator",
|
"type": "separator",
|
||||||
"title": ""
|
"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",
|
"updating": "Updating",
|
||||||
"changelog": {
|
"changelog": {
|
||||||
"title": "Changelog",
|
"title": "Changelog",
|
||||||
"newWindow": true,
|
"newWindow": true,
|
||||||
"href": "https://github.com/TheWidlarzGroup/react-native-video/blob/master/CHANGELOG.md"
|
"href": "https://github.com/TheWidlarzGroup/react-native-video/blob/master/CHANGELOG.md"
|
||||||
},
|
}
|
||||||
"separator_community": {
|
}
|
||||||
"type": "separator",
|
|
||||||
"title": ""
|
|
||||||
},
|
|
||||||
"projects": "Useful projects"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,38 +1,190 @@
|
|||||||
# Ads
|
# Ads
|
||||||
|
|
||||||
## IMA SDK
|
## IMA SDK
|
||||||
`react-native-video` has built-in support for Google IMA SDK for Android and iOS. To enable it please refer to [installation section](/installation)
|
|
||||||
|
`react-native-video` includes built-in support for Google IMA SDK on Android and iOS. To enable it, refer to the [installation section](/installation).
|
||||||
|
|
||||||
|
The IMA SDK supports two types of ad insertion:
|
||||||
|
|
||||||
|
1. **Client-Side Ad Insertion (CSAI)** – Ads are inserted client-side using VAST tags
|
||||||
|
2. **Server-Side Ad Insertion (SSAI)** – Server-side ad insertion where ads are stitched into the stream
|
||||||
|
|
||||||
|
Both ad types are configured through the unified `ad` property in the source configuration, using the `type` field to specify which mode to use.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Client-Side Ad Insertion (CSAI)
|
||||||
|
|
||||||
|
CSAI inserts ads client-side using VAST (Video Ad Serving Template) tags. Ads are requested and played during video playback, with the player handling ad breaks and transitions.
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
To use AVOD, you need to pass `adTagUrl` prop to `Video` component. `adTagUrl` is a VAST uri.
|
|
||||||
|
|
||||||
Example:
|
To use CSAI, configure the `ad` property with `type: 'csai'` and provide an `adTagUrl`. The `adTagUrl` should be a VAST-compliant URI.
|
||||||
```
|
|
||||||
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 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.
|
#### Example:
|
||||||
|
|
||||||
### Events
|
|
||||||
To receive events from IMA SDK, you need to pass `onReceiveAdEvent` prop to `Video` component. List of events, you can find [here](https://github.com/TheWidlarzGroup/react-native-video/blob/master/src/types/Ads.ts)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
...
|
<Video
|
||||||
onReceiveAdEvent={event => console.log(event)}
|
source={{
|
||||||
...
|
uri: 'https://example.com/video.mp4',
|
||||||
|
ad: {
|
||||||
|
type: 'csai',
|
||||||
|
adTagUrl:
|
||||||
|
'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpostoptimizedpodbumper&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator=',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** Video ads cannot start when Picture-in-Picture (PiP) mode is active on iOS. More details are available in the [Google IMA SDK Docs](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/picture_in_picture?hl=en#starting_ads). If you are using custom controls, hide the PiP button when receiving the `STARTED` event from `onReceiveAdEvent` and show it again when receiving the `ALL_ADS_COMPLETED` event.
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
To receive events from the IMA SDK, pass the `onReceiveAdEvent` prop to the `Video` component. The full list of supported events is available [here](https://github.com/TheWidlarzGroup/react-native-video/blob/master/src/types/Ads.ts).
|
||||||
|
|
||||||
|
#### Example:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<Video
|
||||||
|
onReceiveAdEvent={(event) => console.log(event)}
|
||||||
|
// ... other props
|
||||||
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Localization
|
### Localization
|
||||||
To change the language of the IMA SDK, you need to pass `adLanguage` prop to `Video` component. List of supported languages, you can find [here](https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/localization#locale-codes)
|
|
||||||
|
|
||||||
By default, ios will use system language and android will use `en`
|
To change the language of the IMA SDK, pass the `adLanguage` prop within the `ad` configuration. The list of supported languages is available [here](https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/localization#locale-codes).
|
||||||
|
|
||||||
Example:
|
- By default, **iOS** uses the system language, and **Android** defaults to `en` (English).
|
||||||
|
|
||||||
|
#### Example:
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
...
|
<Video
|
||||||
adLanguage="fr"
|
source={{
|
||||||
...
|
uri: 'https://example.com/video.mp4',
|
||||||
|
ad: {
|
||||||
|
type: 'csai',
|
||||||
|
adTagUrl: 'https://example.com/adtag',
|
||||||
|
adLanguage: 'fr',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Server-Side Ad Insertion (SSAI)
|
||||||
|
|
||||||
|
SSAI (Server-Side Ad Insertion) is a server-side ad insertion solution where ads are stitched into the video stream before it reaches the player. This provides a seamless viewing experience with no playback interruptions, as the stream appears as a single continuous video.
|
||||||
|
|
||||||
|
Currently, we support **Google IMA DAI**
|
||||||
|
|
||||||
|
SSAI is ideal for:
|
||||||
|
|
||||||
|
- Live streaming with ad breaks
|
||||||
|
- VOD content with dynamic ad insertion
|
||||||
|
- Scenarios where you want a seamless, uninterrupted viewing experience
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
To use SSAI, configure the `ad` property with `type: 'ssai'` within the `source` prop. SSAI supports both Video On Demand (VOD) and Live streaming.
|
||||||
|
|
||||||
|
#### VOD Example:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<Video
|
||||||
|
source={{
|
||||||
|
ad: {
|
||||||
|
type: 'ssai',
|
||||||
|
streamType: 'vod',
|
||||||
|
contentSourceId: '2548831',
|
||||||
|
videoId: 'tears-of-steel',
|
||||||
|
adTagParameters: {
|
||||||
|
custom_param: 'value',
|
||||||
|
},
|
||||||
|
fallbackUri: 'https://example.com/backup-stream.m3u8',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Live Example:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<Video
|
||||||
|
source={{
|
||||||
|
ad: {
|
||||||
|
type: 'ssai',
|
||||||
|
streamType: 'live',
|
||||||
|
assetKey: 'c-rArva4ShKVIAkNfy6HUQ',
|
||||||
|
adTagParameters: {
|
||||||
|
custom_param: 'value',
|
||||||
|
},
|
||||||
|
fallbackUri: 'https://example.com/backup-stream.m3u8',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
For VOD streams, you must provide:
|
||||||
|
|
||||||
|
- `contentSourceId` – The content source ID
|
||||||
|
- `videoId` – The video ID
|
||||||
|
|
||||||
|
For Live streams, you must provide:
|
||||||
|
|
||||||
|
- `assetKey` – The asset key for the live stream
|
||||||
|
|
||||||
|
Optional properties:
|
||||||
|
|
||||||
|
- `format` – Stream format: `'hls'` (default) or `'dash'`. Android only - iOS automatically detects the format.
|
||||||
|
- `adTagParameters` – Custom key-value pairs to pass as ad tag parameters to the IMA SDK. For a list of supported Ad Manager ad tag parameters, see the [Google Ad Manager documentation](https://support.google.com/admanager/answer/7320899?hl=en#npa).
|
||||||
|
- `fallbackUri` – Fallback stream URI. If the SSAI stream fails to load, the player will automatically fall back to this URI
|
||||||
|
|
||||||
|
> **Note:** The `streamType` field (`'vod'` or `'live'`) is required to specify the type of SSAI stream.
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
SSAI uses the same `onReceiveAdEvent` prop as CSAI to report ad-related events. The full list of supported events is available [here](https://github.com/TheWidlarzGroup/react-native-video/blob/master/src/types/Ads.ts).
|
||||||
|
|
||||||
|
#### Example:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<Video
|
||||||
|
source={{
|
||||||
|
ad: {
|
||||||
|
type: 'ssai',
|
||||||
|
streamType: 'vod',
|
||||||
|
contentSourceId: '2548831',
|
||||||
|
videoId: 'tears-of-steel',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onReceiveAdEvent={(event) => console.log(event)}
|
||||||
|
// ... other props
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
For more details on ad configuration properties, see the [props documentation](/component/props#ad).
|
||||||
|
|
||||||
|
### Fallback Stream
|
||||||
|
|
||||||
|
If the SSAI stream fails to load and a `fallbackUri` is provided, the player will automatically fall back to the fallback stream. This ensures playback continuity even when SSAI services are unavailable.
|
||||||
|
|
||||||
|
### Example App
|
||||||
|
|
||||||
|
For testing and experimenting with SSAI, you can use the `expo-dai` example app located in the `examples/expo-dai` directory. This example app demonstrates SSAI functionality for both VOD and Live streaming scenarios.
|
||||||
|
|
||||||
|
### Differences from CSAI
|
||||||
|
|
||||||
|
| Feature | CSAI | SSAI |
|
||||||
|
| ---------------------- | ------------------------------------- | ------------------------------------------ |
|
||||||
|
| Ad insertion | Client-side | Server-side |
|
||||||
|
| Playback interruptions | Possible during ad breaks | Seamless, no interruptions |
|
||||||
|
| Stream format | Original video + separate ad requests | Single unified stream with ads |
|
||||||
|
| Use case | VOD with pre-defined ad breaks | Live and VOD with server-side ad insertion |
|
||||||
|
| Configuration | `source.ad` with `type: 'csai'` | `source.ad` with `type: 'ssai'` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -2,56 +2,61 @@ import PlatformsList from '../../components/PlatformsList/PlatformsList.tsx';
|
|||||||
|
|
||||||
# DRM
|
# DRM
|
||||||
|
|
||||||
> **Note:** DRM is not supported on visionOS yet.
|
## DRM Example
|
||||||
|
|
||||||
## Provide DRM data (only tested with http/https assets)
|
We provide a sample implementation in the [example app](https://github.com/TheWidlarzGroup/react-native-video/blob/master/examples/common/DRMExample.tsx) demonstrating how to use DRM with `react-native-video`. You’ll need a valid token—visit [our site](https://www.thewidlarzgroup.com/services/free-drm-token-generator-for-video?utm_source=rnv&utm_medium=docs&utm_campaign=drm&utm_id=text) to obtain a **free 24-hour token**.
|
||||||
|
|
||||||
You can provide some configuration to allow DRM playback.
|
## DRM Offline
|
||||||
This feature will disable the use of `TextureView` on Android.
|
|
||||||
|
|
||||||
DRM object allows this members:
|
If you need DRM-protected content available offline, our [Offline Video SDK](https://sdk.thewidlarzgroup.com/offline-video/?utm_source=rnv&utm_medium=docs&utm_campaign=drm&utm_id=offline-video-sdk-link) enables downloading, storing, and managing streams with and without DRM. It also handles many edge cases you may encounter over time.
|
||||||
|
|
||||||
|
### Prerequisites:
|
||||||
|
- Use `react-native-video` v6 or v7. If you're still on v5 or lower, [contact us](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs&utm_campaign=drm&utm_id=upgrade-contact#Contact) for assistance.
|
||||||
|
|
||||||
|
> Supporting our software kits helps maintain this open-source project. Thank you!
|
||||||
|
|
||||||
|
## Providing DRM Data (Tested with HTTP/HTTPS Assets)
|
||||||
|
|
||||||
|
You can configure DRM playback by providing a DRM object with the following properties. This feature disables the use of `TextureView` on Android.
|
||||||
|
|
||||||
|
### DRM Properties
|
||||||
|
|
||||||
### `base64Certificate`
|
### `base64Certificate`
|
||||||
|
|
||||||
<PlatformsList types={['iOS']} />
|
<PlatformsList types={['iOS', 'visionOS']} />
|
||||||
|
|
||||||
Type: bool\
|
**Type:** boolean
|
||||||
Default: false
|
**Default:** `false`
|
||||||
|
|
||||||
Whether or not the certificate url returns it on base64.
|
Indicates whether the certificate URL returns data in Base64 format.
|
||||||
|
|
||||||
### `certificateUrl`
|
### `certificateUrl`
|
||||||
|
|
||||||
<PlatformsList types={['iOS']} />
|
<PlatformsList types={['iOS', 'visionOS']} />
|
||||||
|
|
||||||
Type: string\
|
**Type:** string
|
||||||
Default: undefined
|
**Default:** `undefined`
|
||||||
|
|
||||||
URL to fetch a valid certificate for FairPlay.
|
The URL used to fetch a valid certificate for FairPlay.
|
||||||
|
|
||||||
### `getLicense`
|
### `getLicense`
|
||||||
|
|
||||||
<PlatformsList types={['iOS']} />
|
<PlatformsList types={['iOS', 'visionOS']} />
|
||||||
|
|
||||||
Type: function\
|
**Type:** function
|
||||||
Default: undefined
|
**Default:** `undefined`
|
||||||
|
|
||||||
Rather than setting the `licenseServer` url to get the license, you can manually get the license on the JS part, and send the result to the native part to configure FairplayDRM for the stream
|
Instead of setting `licenseServer`, you can manually acquire the license in JavaScript and send the result to the native module for FairPlay DRM configuration.
|
||||||
|
|
||||||
`licenseServer` and `headers` will be ignored. You will obtain as argument the `SPC`
|
The following parameters are available in `getLicense`:
|
||||||
(as ASCII string, you will probably need to convert it to base 64) obtained from
|
- `contentId`: The content ID from the DRM object or `loadingRequest.request.url?.host`
|
||||||
your `contentId` + the provided certificate via `objc [loadingRequest streamingContentKeyRequestDataForApp:certificateData
|
- `loadedLicenseUrl`: The URL retrieved from `loadingRequest.request.URL.absoluteString`, starting with `skd://` or `clearkey://`
|
||||||
contentIdentifier:contentIdData options:nil error:&spcError]; `
|
- `licenseServer`: The URL passed in the DRM object
|
||||||
|
- `spcString`: The SPC used for DRM validation
|
||||||
|
|
||||||
Also, you will receive following parameter of getLicense:
|
You should return a Base64-encoded CKC response, either directly or as a `Promise`.
|
||||||
* `contentId` contentId if passed to `drm` object or loadingRequest.request.url?.host
|
|
||||||
* `loadedLicenseUrl` URL defined as `loadingRequest.request.URL.absoluteString`, this url starts with `skd://` or `clearkey://`
|
|
||||||
* `licenseServer` prop if prop is passed to `drm` object.
|
|
||||||
* `spcString` the SPC used to validate playback with drm server
|
|
||||||
|
|
||||||
You should return on this method a `CKC` in Base64, either by just returning it or returning a `Promise` that resolves with the `CKC`.
|
#### Example:
|
||||||
|
|
||||||
With this prop you can override the license acquisition flow, as an example:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
getLicense: (spcString, contentId, licenseUrl, loadedLicenseUrl) => {
|
getLicense: (spcString, contentId, licenseUrl, loadedLicenseUrl) => {
|
||||||
@@ -61,143 +66,138 @@ getLicense: (spcString, contentId, licenseUrl, loadedLicenseUrl) => {
|
|||||||
return fetch(`https://license.pallycon.com/ri/licenseManager.do`, {
|
return fetch(`https://license.pallycon.com/ri/licenseManager.do`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'pallycon-customdata-v2':
|
'pallycon-customdata-v2': 'your-custom-header',
|
||||||
'd2VpcmRiYXNlNjRzdHJpbmcgOlAgRGFuaWVsIE1hcmnxbyB3YXMgaGVyZQ==',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
.then((response) => response.text())
|
.then((response) => response.text())
|
||||||
.then((response) => {
|
.then((response) => response)
|
||||||
return response;
|
.catch((error) => console.error('Error', error));
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error', error);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### `contentId`
|
### `contentId`
|
||||||
|
|
||||||
<PlatformsList types={['iOS']} />
|
<PlatformsList types={['iOS', 'visionOS']} />
|
||||||
|
|
||||||
Type: string\
|
**Type:** string
|
||||||
Default: undefined
|
**Default:** `undefined`
|
||||||
|
|
||||||
Specify the content id of the stream, otherwise it will take the host value from `loadingRequest.request.URL.host` (f.e: `skd://testAsset` -> will take `testAsset`)
|
Sets the content ID for the stream. If not specified, the system uses the host value from `loadingRequest.request.URL.host`.
|
||||||
|
|
||||||
### `headers`
|
### `headers`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||||
|
|
||||||
Type: Object\
|
**Type:** Object
|
||||||
Default: undefined
|
**Default:** `undefined`
|
||||||
|
|
||||||
You can customize headers send to the licenseServer.
|
Custom headers for the license server request.
|
||||||
|
|
||||||
Example:
|
#### Example:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
source={{
|
drm: {
|
||||||
uri: 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd',
|
type: DRMType.WIDEVINE,
|
||||||
}}
|
licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense',
|
||||||
drm={{
|
headers: {
|
||||||
type: DRMType.WIDEVINE,
|
'X-AxDRM-Message': 'your-drm-header',
|
||||||
licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense',
|
},
|
||||||
headers: {
|
}
|
||||||
'X-AxDRM-Message': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImZpcnN0X3BsYXlfZXhwaXJhdGlvbiI6NjAsInBsYXlyZWFkeSI6eyJyZWFsX3RpbWVfZXhwaXJhdGlvbiI6dHJ1ZX0sImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.FAbIiPxX8BHi9RwfzD7Yn-wugU19ghrkBFKsaCPrZmU'
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### `licenseServer`
|
### `licenseServer`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||||
|
|
||||||
Type: string\
|
**Type:** string
|
||||||
Default: false
|
**Default:** `undefined`
|
||||||
|
|
||||||
The URL pointing to the licenseServer that will provide the authorization to play the protected stream.
|
The license server URL that authorizes protected content playback.
|
||||||
|
|
||||||
### `multiDrm`
|
### `multiDrm`
|
||||||
<PlatformsList types={['Android']} />
|
|
||||||
Type: boolean\
|
|
||||||
Default: false
|
|
||||||
|
|
||||||
Indicates that drm system shall support key rotation, see: https://developer.android.google.cn/media/media3/exoplayer/drm?hl=en#key-rotation
|
<PlatformsList types={['Android']} />
|
||||||
|
|
||||||
|
**Type:** boolean
|
||||||
|
**Default:** `false`
|
||||||
|
|
||||||
|
Indicates whether the DRM system should support key rotation. See [Android Developer Docs](https://developer.android.google.cn/media/media3/exoplayer/drm?hl=en#key-rotation) for more details.
|
||||||
|
|
||||||
### `type`
|
### `type`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS']} />
|
||||||
|
|
||||||
Type: DRMType\
|
**Type:** DRMType
|
||||||
Default: undefined
|
**Default:** `undefined`
|
||||||
|
|
||||||
You can specify the DRM type, either by string or using the exported DRMType enum.
|
Defines the DRM type:
|
||||||
Valid values are, for Android: DRMType.WIDEVINE / DRMType.PLAYREADY / DRMType.CLEARKEY.
|
- **Android:** `DRMType.WIDEVINE`, `DRMType.PLAYREADY`, `DRMType.CLEARKEY`
|
||||||
for iOS: DRMType.FAIRPLAY
|
- **iOS:** `DRMType.FAIRPLAY`
|
||||||
|
|
||||||
### `localSourceEncryptionKeyScheme`
|
### `localSourceEncryptionKeyScheme`
|
||||||
|
|
||||||
<PlatformsList types={['iOS']} />
|
<PlatformsList types={['iOS', 'visionOS']} />
|
||||||
|
|
||||||
Set the url scheme for stream encryption key for local assets
|
**Type:** string
|
||||||
|
|
||||||
Type: String
|
Sets the URL scheme for stream encryption keys used in local assets.
|
||||||
|
|
||||||
Example:
|
#### Example:
|
||||||
|
|
||||||
```
|
```js
|
||||||
localSourceEncryptionKeyScheme="my-offline-key"
|
localSourceEncryptionKeyScheme="my-offline-key"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Usage Scenarios
|
## Common Usage Scenarios
|
||||||
|
|
||||||
### Send cookies to license server
|
### Sending Cookies to the License Server
|
||||||
|
|
||||||
You can send Cookies to the license server via `headers` prop. Example:
|
You can send cookies using the `headers` prop.
|
||||||
|
|
||||||
|
#### Example:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
drm: {
|
drm: {
|
||||||
type: DRMType.WIDEVINE
|
type: DRMType.WIDEVINE,
|
||||||
licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense',
|
licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense',
|
||||||
headers: {
|
headers: {
|
||||||
'Cookie': 'PHPSESSID=etcetc; csrftoken=mytoken; _gat=1; foo=bar'
|
'Cookie': 'PHPSESSID=your-session-id; csrftoken=mytoken; _gat=1; foo=bar'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom License Acquisition (only iOS for now)
|
### Custom License Acquisition (iOS Only)
|
||||||
|
|
||||||
|
#### Example:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
drm: {
|
drm: {
|
||||||
type: DRMType.FAIRPLAY,
|
type: DRMType.FAIRPLAY,
|
||||||
getLicense: (spcString) => {
|
getLicense: (spcString) => {
|
||||||
const base64spc = Base64.encode(spcString);
|
const base64spc = Base64.encode(spcString);
|
||||||
return fetch('YOUR LICENSE SERVER HERE', {
|
return fetch('YOUR_LICENSE_SERVER_URL', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
getFairplayLicense: {
|
getFairplayLicense: {
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
spcMessage: base64spc,
|
spcMessage: base64spc,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response && response.getFairplayLicenseResponse
|
if (response?.getFairplayLicenseResponse?.ckcResponse) {
|
||||||
&& response.getFairplayLicenseResponse.ckcResponse) {
|
return 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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import PlatformsList from '../../components/PlatformsList/PlatformsList.tsx';
|
|||||||
|
|
||||||
# Events
|
# Events
|
||||||
|
|
||||||
This page shows the list of available callbacks to handle player notifications
|
This page lists all available callbacks for handling player notifications.
|
||||||
|
|
||||||
## Details
|
## Details
|
||||||
|
|
||||||
@@ -10,233 +10,240 @@ This page shows the list of available callbacks to handle player notifications
|
|||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS']} />
|
||||||
|
|
||||||
Callback function that is called when the audio is about to become 'noisy' due to
|
Triggered when audio output changes (e.g., switching from headphones to speakers). It's recommended to pause the media when this event occurs.
|
||||||
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`
|
### `onAudioFocusChanged`
|
||||||
|
|
||||||
<PlatformsList types={['Android']} />
|
<PlatformsList types={['Android']} />
|
||||||
|
|
||||||
Callback function that is called when the audio focus changes. This is called when the audio focus is gained or lost. This is useful for determining if the media should be paused or not.
|
Called when audio focus is gained or lost.
|
||||||
|
|
||||||
Payload:
|
**Payload:**
|
||||||
Property | Type | Description
|
| Property | Type | Description |
|
||||||
--- | --- | ---
|
|---------------|--------|----------------------------------------------|
|
||||||
hasAudioFocus | boolean | Boolean indicating whether the media has audio focus
|
| hasAudioFocus | boolean | `true` if media has audio focus, `false` otherwise |
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
|
**Example:**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
hasAudioFocus: true;
|
hasAudioFocus: true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onAudioTracks`
|
### `onAudioTracks`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS']} />
|
||||||
|
|
||||||
Callback function that is called when audio tracks change
|
Triggered when available audio tracks change.
|
||||||
|
|
||||||
Payload:
|
**Payload:** _Array of objects with track details_
|
||||||
|
|
||||||
An **array** of
|
| Property | Type | Description |
|
||||||
| Property | Type | Description |
|
|----------|--------|-----------------------------------------------------------------------------|
|
||||||
| -------- | ------- | ---------------------------------------------------------------------------------------------------------- |
|
| index | number | Internal track ID |
|
||||||
| index | number | Internal track ID |
|
| title | string | Descriptive track name |
|
||||||
| title | string | Descriptive name for the track |
|
| language | string | [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language code |
|
||||||
| language | string | 2 letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) representing the language |
|
| bitrate | number | Track bitrate |
|
||||||
| bitrate | number | bitrate of track |
|
| type | string | Track MIME type |
|
||||||
| type | string | Mime type of track |
|
| selected | boolean | `true` if track is currently playing |
|
||||||
| selected | boolean | true if track is playing |
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
|
**Example:**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
audioTracks: [
|
audioTracks: [
|
||||||
{ language: 'es', title: 'Spanish', type: 'audio/mpeg', index: 0, selected: true },
|
{ language: 'es', title: 'Spanish', type: 'audio/mpeg', index: 0, selected: true },
|
||||||
{ language: 'en', title: 'English', type: 'audio/mpeg', index: 1 }
|
{ language: 'en', title: 'English', type: 'audio/mpeg', index: 1 }
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onBandwidthUpdate`
|
### `onBandwidthUpdate`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS']} />
|
||||||
|
|
||||||
Callback function that is called when the available bandwidth changes.
|
Called when available bandwidth changes.
|
||||||
|
|
||||||
Payload:
|
**Payload:**
|
||||||
|
| Property | Type | Description |
|
||||||
| Property | Type | Description |
|
|----------|--------|-----------------------------------------------|
|
||||||
| -------- | ------ | ---------------------------------------------- |
|
| bitrate | number | Estimated bitrate in bits/sec |
|
||||||
| bitrate | number | The estimated bitrate in bits/sec |
|
| width | number | Video width (Android only) |
|
||||||
| width | number | The width of the video (android only) |
|
| height | number | Video height (Android only) |
|
||||||
| height | number | The height of the video (android only) |
|
| trackId | string | Video track ID (Android only) |
|
||||||
| trackId | string | The track ID of the video track (android only) |
|
|
||||||
|
|
||||||
Example on iOS:
|
|
||||||
|
|
||||||
|
**Example (iOS):**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
bitrate: 1000000;
|
bitrate: 1000000
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Example on Android:
|
**Example (Android):**
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
bitrate: 1000000;
|
bitrate: 1000000,
|
||||||
width: 1920;
|
width: 1920,
|
||||||
height: 1080;
|
height: 1080,
|
||||||
trackId: 'some-track-id';
|
trackId: 'some-track-id'
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: On Android, you must set the [reportBandwidth](#reportbandwidth) prop to enable this event. This is due to the high volume of events generated.
|
> **Note:** On Android, set the [`reportBandwidth`](#reportbandwidth) prop to enable this event.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onBuffer`
|
### `onBuffer`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
Callback function that is called when the player buffers.
|
Triggered when buffering starts or stops.
|
||||||
|
|
||||||
Payload:
|
**Payload:**
|
||||||
|
| Property | Type | Description |
|
||||||
| Property | Type | Description |
|
|------------|--------|---------------------------------|
|
||||||
| ----------- | ------- | ---------------------------------------------- |
|
| isBuffering | boolean | `true` if buffering is active |
|
||||||
| isBuffering | boolean | Boolean indicating whether buffering is active |
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
|
**Example:**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
isBuffering: true;
|
isBuffering: true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onControlsVisibilityChange`
|
### `onControlsVisibilityChange`
|
||||||
|
|
||||||
<PlatformsList types={['Android']} />
|
<PlatformsList types={['Android']} />
|
||||||
|
|
||||||
Callback function that is called when the controls are hidden or shown. Not possible on iOS.
|
Triggered when the video player controls become visible or hidden.
|
||||||
|
|
||||||
Payload:
|
**Payload:**
|
||||||
|
| Property | Type | Description |
|
||||||
| Property | Type | Description |
|
|----------|--------|-------------------------------------|
|
||||||
| ----------- | ------- | ---------------------------------------------- |
|
| isVisible | boolean | `true` if controls are visible |
|
||||||
| isVisible | boolean | Boolean indicating whether controls are visible |
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
|
**Example:**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
isVisible: true;
|
isVisible: true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onEnd`
|
### `onEnd`
|
||||||
|
|
||||||
<PlatformsList types={['All']} />
|
<PlatformsList types={['All']} />
|
||||||
|
|
||||||
Callback function that is called when the player reaches the end of the media.
|
Triggered when the media reaches the end.
|
||||||
|
|
||||||
Payload: none
|
**Payload:** _none_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onError`
|
### `onError`
|
||||||
|
|
||||||
<PlatformsList types={['All']} />
|
<PlatformsList types={['All']} />
|
||||||
|
|
||||||
Callback function that is called when the player experiences a playback error.
|
Called when a playback error occurs.
|
||||||
|
|
||||||
Payload:
|
**Payload:**
|
||||||
|
| Property | Type | Description |
|
||||||
|
|---------|--------|---------------------------|
|
||||||
|
| error | object | Error details |
|
||||||
|
|
||||||
| Property | Type | Description |
|
---
|
||||||
| -------- | ------ | ------------------------------------------------------------- |
|
|
||||||
| error | object | Object containing properties with information about the error |
|
|
||||||
|
|
||||||
### `onExternalPlaybackChange`
|
### `onExternalPlaybackChange`
|
||||||
|
|
||||||
<PlatformsList types={['iOS']} />
|
<PlatformsList types={['iOS']} />
|
||||||
|
|
||||||
Callback function that is called when external playback mode for current playing video has changed. Mostly useful when connecting/disconnecting to Apple TV – it's called on connection/disconnection.
|
Called when external playback mode changes (e.g., Apple TV connection/disconnection).
|
||||||
|
|
||||||
Payload:
|
**Payload:**
|
||||||
|
| Property | Type | Description |
|
||||||
| Property | Type | Description |
|
|-------------------------|--------|--------------------------------------------|
|
||||||
| ------------------------ | ------- | ----------------------------------------------------------- |
|
| isExternalPlaybackActive | boolean | `true` if external playback is active |
|
||||||
| isExternalPlaybackActive | boolean | Boolean indicating whether external playback mode is active |
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
|
**Example:**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
isExternalPlaybackActive: true;
|
isExternalPlaybackActive: true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onFullscreenPlayerWillPresent`
|
### `onFullscreenPlayerWillPresent`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||||
|
|
||||||
Callback function that is called when the player is about to enter fullscreen mode.
|
Called before entering fullscreen mode.
|
||||||
|
|
||||||
Payload: none
|
**Payload:** _none_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onFullscreenPlayerDidPresent`
|
### `onFullscreenPlayerDidPresent`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||||
|
|
||||||
Callback function that is called when the player has entered fullscreen mode.
|
Called when fullscreen mode is active.
|
||||||
|
|
||||||
Payload: none
|
**Payload:** _none_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onFullscreenPlayerWillDismiss`
|
### `onFullscreenPlayerWillDismiss`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||||
|
|
||||||
Callback function that is called when the player is about to exit fullscreen mode.
|
Called before exiting fullscreen mode.
|
||||||
|
|
||||||
Payload: none
|
**Payload:** _none_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onFullscreenPlayerDidDismiss`
|
### `onFullscreenPlayerDidDismiss`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||||
|
|
||||||
Callback function that is called when the player has exited fullscreen mode.
|
Called when fullscreen mode is exited.
|
||||||
|
|
||||||
Payload: none
|
**Payload:** _none_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onLoad`
|
### `onLoad`
|
||||||
|
|
||||||
<PlatformsList types={['All']} />
|
<PlatformsList types={['All']} />
|
||||||
|
|
||||||
Callback function that is called when the media is loaded and ready to play.
|
Triggered when the media is loaded and ready to play.
|
||||||
|
|
||||||
|
### Payload:
|
||||||
NOTE: tracks (`audioTracks`, `textTracks` & `videoTracks`) are not available on the web.
|
|
||||||
|
|
||||||
Payload:
|
|
||||||
|
|
||||||
| Property | Type | Description |
|
| Property | Type | Description |
|
||||||
|-------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| currentTime | number | Time in seconds where the media will start |
|
| currentTime | number | Time in seconds where the media will start |
|
||||||
| duration | number | Length of the media in seconds |
|
| duration | number | Length of the media in seconds |
|
||||||
| naturalSize | object | Properties:<br/> _ width - Width in pixels that the video was encoded at<br/> _ height - Height in pixels that the video was encoded at<br/> \* orientation - "portrait", "landscape" or "square" |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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`. |
|
| trackId | string | Provide key information about the video track, typically including: `Resolution`, `Bitrate`. |
|
||||||
|
|
||||||
Example:
|
**Example:**
|
||||||
|
|
||||||
```javascript
|
```js
|
||||||
{
|
{
|
||||||
canPlaySlowForward: true,
|
canPlaySlowForward: true,
|
||||||
canPlayReverse: false,
|
canPlayReverse: false,
|
||||||
@@ -265,49 +272,51 @@ Example:
|
|||||||
{ index: 1, bitrate: 7981888, codecs: "avc1.640028", height: 1080, trackId: "f2-v1-x3", width: 1920 },
|
{ 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 }
|
{ 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`
|
### `onLoadStart`
|
||||||
|
|
||||||
<PlatformsList types={['All']} />
|
<PlatformsList types={['All']} />
|
||||||
|
|
||||||
Callback function that is called when the media starts loading.
|
Triggered when media starts loading.
|
||||||
|
|
||||||
Payload:
|
**Payload:**
|
||||||
|
| Property | Type | Description |
|
||||||
| Property | Type | Description |
|
|----------|--------|-------------------------------------|
|
||||||
| --------- | ----------- | ---------------------------------------------------------------- |
|
| isNetwork | boolean | `true` if media is loaded from a network |
|
||||||
| isNetwork | boolean | Boolean indicating if the media is being loaded from the network |
|
| type | string | Media type (not available on Windows) |
|
||||||
| type | string | Type of the media. Not available on Windows |
|
| uri | string | Media source URI (not available on Windows) |
|
||||||
| uri | string | URI for the media source. Not available on Windows |
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
|
**Example:**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
isNetwork: true,
|
isNetwork: true,
|
||||||
type: '',
|
type: '',
|
||||||
uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8'
|
uri: 'https://example.com/video.mp4'
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onPlaybackStateChanged`
|
### `onPlaybackStateChanged`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
|
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
|
||||||
|
|
||||||
Callback function that is called when the playback state changes.
|
Triggered when playback state changes.
|
||||||
|
|
||||||
Payload:
|
**Payload:**
|
||||||
|
| Property | Type | Description |
|
||||||
| Property | Type | Description |
|
|----------|--------|-------------------------------------|
|
||||||
| --------- | ----------- | -------------------------------------------------- |
|
| isPlaying | boolean | `true` if media is playing |
|
||||||
| isPlaying | boolean | Boolean indicating if the media is playing or not |
|
| isSeeking | boolean | `true` if seeking is in progress |
|
||||||
| isSeeking | boolean | Boolean indicating if the player is seeking or not |
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
|
**Example:**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
isPlaying: true,
|
isPlaying: true,
|
||||||
@@ -315,56 +324,62 @@ Example:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onPictureInPictureStatusChanged`
|
### `onPictureInPictureStatusChanged`
|
||||||
|
|
||||||
<PlatformsList types={['iOS']} />
|
<PlatformsList types={['iOS', 'Android', 'web']} />
|
||||||
|
|
||||||
Callback function that is called when picture in picture becomes active or inactive.
|
Triggered when Picture-in-Picture (PiP) mode is activated or deactivated.
|
||||||
|
|
||||||
| Property | Type | Description |
|
**Payload:**
|
||||||
| -------- | ------- | ------------------------------------------------------- |
|
| Property | Type | Description |
|
||||||
| isActive | boolean | Boolean indicating whether picture in picture is active |
|
|----------|--------|----------------------------------|
|
||||||
|
| isActive | boolean | `true` if PiP mode is active |
|
||||||
Example:
|
|
||||||
|
|
||||||
|
**Example:**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
isActive: true;
|
isActive: true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onPlaybackRateChange`
|
### `onPlaybackRateChange`
|
||||||
|
|
||||||
<PlatformsList types={['All']} />
|
<PlatformsList types={['All']} />
|
||||||
|
|
||||||
Callback function that is called when the rate of playback changes - either paused or starts/resumes.
|
Triggered when playback speed changes.
|
||||||
|
|
||||||
| Property | Type | Description |
|
**Payload:**
|
||||||
| ------------ | ------ | --------------------------------------------------------------------------------------------------------------- |
|
| Property | Type | Description |
|
||||||
| playbackRate | number | 0 when playback is paused, 1 when playing at normal speed. Other values when playback is slowed down or sped up |
|
|-------------|--------|---------------------------------|
|
||||||
|
| playbackRate | number | `0` (paused), `1` (normal speed), other values indicate speed changes |
|
||||||
Example:
|
|
||||||
|
|
||||||
|
**Example:**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
playbackRate: 0, // indicates paused
|
playbackRate: 0 // indicates paused
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onProgress`
|
### `onProgress`
|
||||||
|
|
||||||
<PlatformsList types={['All']} />
|
<PlatformsList types={['All']} />
|
||||||
|
|
||||||
Callback function that is called every progressUpdateInterval milliseconds with info about which position the media is currently playing.
|
Triggered every `progressUpdateInterval` milliseconds, providing information about the current playback position.
|
||||||
|
|
||||||
| Property | Type | Description |
|
**Payload:**
|
||||||
| ---------------- | ------ | ------------------------------------------------------------------------------------------------- |
|
| Property | Type | Description |
|
||||||
| currentTime | number | Current position in seconds |
|
|----------------|--------|-------------------------------------------------------------------------|
|
||||||
| playableDuration | number | Position to where the media can be played to using just the buffer in seconds |
|
| currentTime | number | Current playback position (seconds) |
|
||||||
| seekableDuration | number | Position to where the media can be seeked to in seconds. Typically, the total length of the media |
|
| playableDuration | number | Duration that can be played using only the buffer (seconds) |
|
||||||
|
| seekableDuration | number | Duration that can be seeked to (usually the total length of the media) |
|
||||||
Example:
|
|
||||||
|
|
||||||
|
**Example:**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
currentTime: 5.2,
|
currentTime: 5.2,
|
||||||
@@ -373,80 +388,82 @@ Example:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onReadyForDisplay`
|
### `onReadyForDisplay`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'Web']} />
|
<PlatformsList types={['Android', 'iOS', 'Web']} />
|
||||||
|
|
||||||
Callback function that is called when the first video frame is ready for display. This is when the poster is removed.
|
Triggered when the first video frame is ready to be displayed. This is when the poster is removed.
|
||||||
|
|
||||||
Payload: none
|
**Payload:** _none_
|
||||||
|
|
||||||
- iOS: [readyForDisplay](https://developer.apple.com/documentation/avkit/avplayerviewcontroller/1615830-readyfordisplay?language=objc)
|
- 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)
|
- Android: [`STATE_READY`](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#STATE_READY)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onReceiveAdEvent`
|
### `onReceiveAdEvent`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS']} />
|
||||||
|
|
||||||
Callback function that is called when an AdEvent is received from the IMA's SDK.
|
Triggered when an AdEvent is received from the IMA SDK.
|
||||||
|
|
||||||
Enum `AdEvent` possible values for [Android](https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdEvent) and [iOS](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/reference/Enums/IMAAdEventType):
|
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>
|
<details>
|
||||||
<summary>Events</summary>
|
<summary>AdEvent</summary>
|
||||||
|
|
||||||
| Event | Platform | Description |
|
| 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_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_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_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_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_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_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_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_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. |
|
| `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. |
|
| `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. |
|
| `CLICK` | Android, iOS | Fires when the ad is clicked. |
|
||||||
| `COMPLETED` | Android, iOS | Fires when the ad completes playing. |
|
| `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_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. |
|
| `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). |
|
| `CUEPOINTS_CHANGED` | iOS | Cuepoints changed for VOD stream (only used for dynamic ad insertion). |
|
||||||
| `DURATION_CHANGE` | Android | Fires when the ad's duration changes. |
|
| `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. |
|
| `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. |
|
| `FIRST_QUARTILE` | Android, iOS | Fires when the ad playhead crosses first quartile. |
|
||||||
| `IMPRESSION` | Android | Fires when the impression URL has been pinged. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `MIDPOINT` | Android, iOS | Fires when the ad playhead crosses midpoint. |
|
||||||
| `PAUSED` | Android, iOS | Fires when the ad is paused. |
|
| `PAUSED` | Android, iOS | Fires when the ad is paused. |
|
||||||
| `RESUMED` | Android, iOS | Fires when the ad is resumed. |
|
| `RESUMED` | Android, iOS | Fires when the ad is resumed. |
|
||||||
| `SKIPPABLE_STATE_CHANGED` | Android | Fires when the displayed ads skippable state is changed. |
|
| `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. |
|
| `SKIPPED` | Android, iOS | Fires when the ad is skipped by the user. |
|
||||||
| `STARTED` | Android, iOS | Fires when the ad starts playing. |
|
| `STARTED` | Android, iOS | Fires when the ad starts playing. |
|
||||||
| `STREAM_LOADED` | iOS | Stream request has loaded (only used for dynamic ad insertion). |
|
| `STREAM_LOADED` | iOS | Stream request has loaded (only used for dynamic ad insertion). |
|
||||||
| `TAPPED` | iOS | Fires when the ad is tapped. |
|
| `TAPPED` | iOS | Fires when the ad is tapped. |
|
||||||
| `THIRD_QUARTILE` | Android, iOS | Fires when the ad playhead crosses third quartile. |
|
| `THIRD_QUARTILE` | Android, iOS | Fires when the ad playhead crosses third quartile. |
|
||||||
| `UNKNOWN` | iOS | An unknown event has fired |
|
| `UNKNOWN` | iOS | An unknown event has fired |
|
||||||
| `USER_CLOSE` | Android | Fires when the ad is closed by the user. |
|
| `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_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. |
|
| `VIDEO_ICON_CLICKED` | Android | Fires when a user clicks a video icon. |
|
||||||
| `VOLUME_CHANGED` | Android | Fires when the ad volume has changed. |
|
| `VOLUME_CHANGED` | Android | Fires when the ad volume has changed. |
|
||||||
| `VOLUME_MUTED` | Android | Fires when the ad volume has been muted. |
|
| `VOLUME_MUTED` | Android | Fires when the ad volume has been muted. |
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
Payload:
|
**Payload:**
|
||||||
|
|
||||||
| Property | Type | Description |
|
| Property | Type | Description |
|
||||||
| -------- | ----------------------------------------- | --------------------- |
|
|----------|-----------------------------------------|---------------------|
|
||||||
| event | AdEvent | The ad event received |
|
| event | AdEvent | The ad event received |
|
||||||
| data | Record<string, string> \| undefined | The ad event data |
|
| data | Record<string, string> \| undefined | Additional ad event data |
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
|
**Example:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
@@ -456,173 +473,172 @@ Example:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onRestoreUserInterfaceForPictureInPictureStop`
|
### `onRestoreUserInterfaceForPictureInPictureStop`
|
||||||
|
|
||||||
<PlatformsList types={['iOS', 'visionOS']} />
|
<PlatformsList types={['iOS', 'visionOS']} />
|
||||||
|
|
||||||
Callback function that corresponds to Apple's [`restoreUserInterfaceForPictureInPictureStopWithCompletionHandler`](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). Call `restoreUserInterfaceForPictureInPictureStopCompleted` inside of this function when done restoring the user interface.
|
Corresponds to Apple's [`restoreUserInterfaceForPictureInPictureStopWithCompletionHandler`](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc).
|
||||||
|
Call `restoreUserInterfaceForPictureInPictureStopCompleted` inside this function when the UI is restored.
|
||||||
|
|
||||||
Payload: none
|
**Payload:** _none_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onSeek`
|
### `onSeek`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'Windows UWP', 'web']} />
|
<PlatformsList types={['Android', 'iOS', 'Windows UWP', 'web']} />
|
||||||
|
|
||||||
Callback function that is called when a seek completes.
|
Triggered when a seek operation completes.
|
||||||
|
|
||||||
Payload:
|
|
||||||
|
|
||||||
|
**Payload:**
|
||||||
| Property | Type | Description |
|
| Property | Type | Description |
|
||||||
| ----------- | ------ | ------------------------------- |
|
|------------|--------|---------------------------------|
|
||||||
| currentTime | number | The current time after the seek |
|
| currentTime | number | Current time after seeking |
|
||||||
| seekTime | number | The requested time |
|
| seekTime | number | Requested seek time |
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
|
**Example:**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
currentTime: 100.5;
|
currentTime: 100.5,
|
||||||
seekTime: 100;
|
seekTime: 100
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Both the currentTime & seekTime are reported because the video player may not seek to the exact requested position in order to improve seek performance.
|
> **Note:** On iOS, this callback is not reported when native controls are enabled.
|
||||||
|
|
||||||
Note: on iOS, when controls are enable, this callback is not reported. This is a known limitation.
|
---
|
||||||
|
|
||||||
### `onTimedMetadata`
|
### `onTimedMetadata`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||||
|
|
||||||
Callback function that is called when timed metadata becomes available
|
Triggered when timed metadata is available.
|
||||||
|
|
||||||
Payload:
|
|
||||||
|
|
||||||
|
**Payload:**
|
||||||
| Property | Type | Description |
|
| Property | Type | Description |
|
||||||
| -------- | ----- | ------------------------- |
|
|----------|------|--------------------------|
|
||||||
| metadata | array | Array of metadata objects |
|
| metadata | array | Array of metadata objects |
|
||||||
|
|
||||||
Example:
|
**Example:**
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
metadata: [
|
metadata: [
|
||||||
{value: 'Streaming Encoder', identifier: 'TRSN'},
|
{ value: 'Streaming Encoder', identifier: 'TRSN' },
|
||||||
{value: 'Internet Stream', identifier: 'TRSO'},
|
{ value: 'Internet Stream', identifier: 'TRSO' },
|
||||||
{value: 'Any Time You Like', identifier: 'TIT2'},
|
{ value: 'Any Time You Like', identifier: 'TIT2' }
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onTextTrackDataChanged`
|
### `onTextTrackDataChanged`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS']} />
|
||||||
|
|
||||||
Callback function that is called when new subtitle data is available. It provides the actual subtitle content for the current selected text track, if available (mainly WebVTT).
|
Triggered when new subtitle data becomes available.
|
||||||
|
|
||||||
Payload:
|
**Payload:**
|
||||||
|
| Property | Type | Description |
|
||||||
| 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
|
```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`
|
### `onTextTracks`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS']} />
|
||||||
|
|
||||||
Callback function that is called when text tracks change
|
Triggered when available text (subtitle) tracks change.
|
||||||
|
|
||||||
Payload:
|
**Payload:**
|
||||||
|
| Property | Type | Description |
|
||||||
| Property | Type | Description |
|
|----------|--------|--------------------------------------------------------------------------------------------------------------|
|
||||||
| -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| index | number | Internal track ID |
|
||||||
| index | number | Internal track ID |
|
| title | string | Track name |
|
||||||
| title | string | Descriptive name for the track |
|
| language | string | 2 letter [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language code |
|
||||||
| language | string | 2 letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) representing the language |
|
| type | string | Track MIME type (_VTT_, _SRT_, _TTML_) |
|
||||||
| 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 the track is currently playing |
|
||||||
| selected | boolean | true if track is playing |
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
|
**Example:**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
textTracks: [
|
textTracks: [
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
title: 'Any Time You Like',
|
title: 'English Subtitles',
|
||||||
type: 'srt',
|
type: 'vtt',
|
||||||
selected: true,
|
selected: true
|
||||||
},
|
}
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onVideoTracks`
|
### `onVideoTracks`
|
||||||
|
|
||||||
<PlatformsList types={['Android']} />
|
<PlatformsList types={['Android']} />
|
||||||
|
|
||||||
Callback function that is called when video tracks change
|
Triggered when video tracks change.
|
||||||
|
|
||||||
Payload:
|
**Payload:**
|
||||||
|
| Property | Type | Description |
|
||||||
| Property | Type | Description |
|
|----------|---------|-----------------------------------|
|
||||||
| -------- | ------- | --------------------------------------------------------------- |
|
| index | number | Track index |
|
||||||
| index | number | index of the track |
|
| trackId | string | Internal track ID |
|
||||||
| trackId | string | Internal track ID |
|
| codecs | string | Codec type |
|
||||||
| codecs | string | MimeType of codec used for this track |
|
| width | number | Video width |
|
||||||
| width | number | Track width |
|
| height | number | Video height |
|
||||||
| height | number | Track height |
|
| bitrate | number | Track bitrate (bps) |
|
||||||
| bitrate | number | Bitrate in bps |
|
| selected | boolean | `true` if the track is playing |
|
||||||
| selected | boolean | true if track is selected for playing |
|
| rotation | number | Rotation angle (0, 90, 180, 270) |
|
||||||
| rotation | number | 0, 90, 180 or 270 rotation to apply to the track (android only) |
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
|
**Example:**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
videoTracks: [
|
videoTracks: [
|
||||||
{
|
{
|
||||||
index: O,
|
index: 0,
|
||||||
trackId: "0",
|
trackId: "1",
|
||||||
codecs: 'video/mp4',
|
codecs: "video/mp4",
|
||||||
width: 1920,
|
width: 1920,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
bitrate: 10000,
|
bitrate: 5000000,
|
||||||
selected: true,
|
selected: true,
|
||||||
rotation: 0,
|
rotation: 0
|
||||||
},
|
}
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `onVolumeChange`
|
### `onVolumeChange`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
|
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
|
||||||
|
|
||||||
Callback function that is called when the volume of player changes.
|
Triggered when the player volume changes.
|
||||||
|
|
||||||
> Note: This event applies to the volume of the player, not the volume of the device.
|
> **Note:** This event applies to the player's volume, not the device's system volume.
|
||||||
|
|
||||||
Payload:
|
**Payload:**
|
||||||
|
| Property | Type | Description |
|
||||||
| Property | Type | Description |
|
|----------|--------|---------------------------------|
|
||||||
| -------- | ------ | ------------------------------------------ |
|
| volume | number | Volume level (0 to 1) |
|
||||||
| volume | number | The volume of the player (between 0 and 1) |
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
|
**Example:**
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
volume: 0.5;
|
volume: 0.5
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,234 +2,336 @@ import PlatformsList from '../../components/PlatformsList/PlatformsList.tsx';
|
|||||||
|
|
||||||
# Methods
|
# Methods
|
||||||
|
|
||||||
This page shows the list of available methods
|
This page shows the list of available methods.
|
||||||
|
|
||||||
|
## Details
|
||||||
|
|
||||||
### `dismissFullscreenPlayer`
|
### `dismissFullscreenPlayer`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
`dismissFullscreenPlayer(): Promise<void>`
|
```tsx
|
||||||
|
dismissFullscreenPlayer(): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
Take the player out of fullscreen mode.
|
Exits fullscreen mode.
|
||||||
|
|
||||||
> [!WARNING]
|
> **Deprecated:** Use `setFullScreen(false)` instead.
|
||||||
> deprecated, use setFullScreen method instead
|
|
||||||
|
---
|
||||||
|
|
||||||
### `pause`
|
### `pause`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
`pause(): Promise<void>`
|
```tsx
|
||||||
|
pause(): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
Pause the video.
|
Pauses the video.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `presentFullscreenPlayer`
|
### `presentFullscreenPlayer`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
`presentFullscreenPlayer(): Promise<void>`
|
```tsx
|
||||||
|
presentFullscreenPlayer(): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
Put the player in fullscreen mode.
|
Enters fullscreen mode.
|
||||||
|
|
||||||
On iOS, this displays the video in a fullscreen view controller with controls.
|
- On **iOS**, this opens a fullscreen view controller with controls.
|
||||||
|
- On **Android**, this makes the player fullscreen but requires styling to match screen dimensions.
|
||||||
|
|
||||||
On Android, this puts the navigation controls in fullscreen mode. It is not a complete fullscreen implementation, so you will still need to apply a style that makes the width and height match your screen dimensions to get a fullscreen video.
|
> **Deprecated:** Use `setFullScreen(true)` instead.
|
||||||
|
|
||||||
> [!WARNING]
|
---
|
||||||
> deprecated, use setFullScreen method instead
|
|
||||||
|
|
||||||
### `resume`
|
### `resume`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
`resume(): Promise<void>`
|
```tsx
|
||||||
|
resume(): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
Resume the video.
|
Resumes video playback.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `restoreUserInterfaceForPictureInPictureStopCompleted`
|
### `restoreUserInterfaceForPictureInPictureStopCompleted`
|
||||||
|
|
||||||
<PlatformsList types={['iOS']} />
|
<PlatformsList types={['iOS']} />
|
||||||
|
|
||||||
`restoreUserInterfaceForPictureInPictureStopCompleted(restored)`
|
```tsx
|
||||||
|
restoreUserInterfaceForPictureInPictureStopCompleted(restored)
|
||||||
|
```
|
||||||
|
|
||||||
This function corresponds to the completion handler in Apple's [restoreUserInterfaceForPictureInPictureStop](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). IMPORTANT: This function must be called after `onRestoreUserInterfaceForPictureInPictureStop` is called.
|
Must be called after `onRestoreUserInterfaceForPictureInPictureStop`.
|
||||||
|
Corresponds to Apple's [`restoreUserInterfaceForPictureInPictureStop`](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `save`
|
### `save`
|
||||||
|
|
||||||
<PlatformsList types={['iOS']} />
|
<PlatformsList types={['iOS']} />
|
||||||
|
|
||||||
`save(): Promise<{ uri: string }>`
|
```tsx
|
||||||
|
save(): Promise<{ uri: string }>
|
||||||
|
```
|
||||||
|
|
||||||
Save video to your Photos with current filter prop. Returns promise.
|
Saves the video to the user's **Photos app** with the current filter.
|
||||||
|
|
||||||
Notes:
|
#### Notes:
|
||||||
|
- Supports **MP4** export only.
|
||||||
|
- Exports to the **cache directory** with a generated UUID filename.
|
||||||
|
- Requires **internet connection** if the video is not already buffered.
|
||||||
|
- Video remains in the **Photos app** until manually deleted.
|
||||||
|
- Works with **cached videos**.
|
||||||
|
|
||||||
- Currently only supports highest quality export
|
#### Future improvements:
|
||||||
- Currently only supports MP4 export
|
- Support for **multiple quality options**.
|
||||||
- Currently only supports exporting to user's cache directory with a generated UUID filename.
|
- Support for **more formats**.
|
||||||
- User will need to remove the saved video through their Photos app
|
- Support for **custom directory and filename**.
|
||||||
- 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:
|
---
|
||||||
|
|
||||||
- Will support multiple qualities through options
|
### `enterPictureInPicture`
|
||||||
- Will support more formats in the future through options
|
|
||||||
- Will support custom directory and file name through options
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
enterPictureInPicture()
|
||||||
|
```
|
||||||
|
|
||||||
|
Activates Picture-in-Picture (PiP) mode.
|
||||||
|
|
||||||
|
#### Android setup:
|
||||||
|
For **Expo**, enable PiP in `app.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"react-native-video",
|
||||||
|
{
|
||||||
|
"enableAndroidPictureInPicture": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
For **Bare React Native**, update `AndroidManifest.xml`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:supportsPictureInPicture="true">
|
||||||
|
</activity>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:**
|
||||||
|
> - On **Android**, entering PiP moves the app to the **background**.
|
||||||
|
> - On **iOS**, **video ads cannot start** in PiP mode ([Google IMA SDK](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/picture_in_picture?hl=en#starting_ads)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `exitPictureInPicture`
|
||||||
|
|
||||||
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
exitPictureInPicture()
|
||||||
|
```
|
||||||
|
|
||||||
|
Exits Picture-in-Picture (PiP) mode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `seek`
|
### `seek`
|
||||||
|
|
||||||
<PlatformsList types={['All']} />
|
<PlatformsList types={['All']} />
|
||||||
|
|
||||||
`seek(seconds)`
|
```tsx
|
||||||
|
seek(seconds: number)
|
||||||
|
```
|
||||||
|
|
||||||
Seek to the specified position represented by seconds. seconds is a float value.
|
Seeks to the specified position (**in seconds**).
|
||||||
|
|
||||||
`seek()` can only be called after the `onLoad` event has fired. Once completed, the [onSeek](./events#onseek) event will be called.
|
#### Notes:
|
||||||
|
- **Must be called after** `onLoad`.
|
||||||
|
- Triggers the [`onSeek`](./events#onseek) event.
|
||||||
|
|
||||||
#### Exact seek
|
#### **iOS Exact Seek:**
|
||||||
|
```tsx
|
||||||
|
seek(seconds, tolerance: number)
|
||||||
|
```
|
||||||
|
- Default **tolerance**: ±100ms.
|
||||||
|
- Set `tolerance = 0` for **precise seeking**.
|
||||||
|
|
||||||
<PlatformsList types={['iOS']} />
|
---
|
||||||
|
|
||||||
By default iOS seeks within 100 milliseconds of the target position. If you need more accuracy, you can use the seek with tolerance method:
|
|
||||||
|
|
||||||
`seek(seconds, tolerance)`
|
|
||||||
|
|
||||||
tolerance is the max distance in milliseconds from the seconds position that's allowed. Using a more exact tolerance can cause seeks to take longer. If you want to seek exactly, set tolerance to 0.
|
|
||||||
|
|
||||||
### `setVolume`
|
### `setVolume`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
`setVolume(value): Promise<void>`
|
```tsx
|
||||||
|
setVolume(value: number): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
This function will change the volume exactly like [volume](./props#volume) property. default value and range are the same then.
|
Changes the **volume** level. Same behavior as the [`volume`](./props#volume) prop.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `getCurrentPosition`
|
### `getCurrentPosition`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
`getCurrentPosition(): Promise<number>`
|
```tsx
|
||||||
|
getCurrentPosition(): Promise<number>
|
||||||
|
```
|
||||||
|
|
||||||
This function retrieves and returns the precise current position of the video playback, measured in seconds.
|
Returns the **current playback position** in seconds.
|
||||||
This function will throw an error if player is not initialized.
|
|
||||||
|
|
||||||
|
> **Throws an error** if the player is not initialized.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `setSource`
|
### `setSource`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS']} />
|
<PlatformsList types={['Android', 'iOS']} />
|
||||||
|
|
||||||
`setSource(source: ReactVideoSource): Promise<void>`
|
```tsx
|
||||||
|
setSource(source: ReactVideoSource): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
This function will change the source exactly like [source](./props#source) property.
|
Updates the media source **dynamically**.
|
||||||
Changing source with this function will overide source provided as props.
|
|
||||||
|
> **Note:** This **overrides** the `source` prop.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `setFullScreen`
|
### `setFullScreen`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
`setFullScreen(fullscreen): Promise<void>`
|
```tsx
|
||||||
|
setFullScreen(fullscreen: boolean): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
If you set it to `true`, the player enters fullscreen mode. If you set it to `false`, the player exits fullscreen mode.
|
Toggles fullscreen mode.
|
||||||
|
|
||||||
On iOS, this displays the video in a fullscreen view controller with controls.
|
- `true` → Enters fullscreen.
|
||||||
|
- `false` → Exits fullscreen.
|
||||||
|
|
||||||
On Android, this puts the navigation controls in fullscreen mode. It is not a complete fullscreen implementation, so you will still need to apply a style that makes the width and height match your screen dimensions to get a fullscreen video.
|
---
|
||||||
|
|
||||||
### `nativeHtmlVideoRef`
|
### `nativeHtmlVideoRef`
|
||||||
|
|
||||||
<PlatformsList types={['web']} />
|
<PlatformsList types={['web']} />
|
||||||
|
|
||||||
A ref to the underlying html video element. This can be used if you need to integrate a 3d party, web only video library (like hls.js, shaka, video.js...).
|
A **reference to the native HTML `<video>` element**.
|
||||||
|
Useful for integrating **third-party** video libraries like **hls.js, shaka, video.js, etc.**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Example Usage
|
### **Example Usage**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const videoRef = useRef<VideoRef>(null);
|
const videoRef = useRef<VideoRef>(null);
|
||||||
|
|
||||||
const someCoolFunctions = async () => {
|
const handleVideoControls = async () => {
|
||||||
if (!videoRef.current) {
|
if (!videoRef.current) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// present or dismiss fullscreen player
|
// Fullscreen controls
|
||||||
videoRef.current.presentFullscreenPlayer();
|
videoRef.current.presentFullscreenPlayer();
|
||||||
videoRef.current.dismissFullscreenPlayer();
|
videoRef.current.dismissFullscreenPlayer();
|
||||||
|
|
||||||
// pause or resume the video
|
// Playback controls
|
||||||
videoRef.current.pause();
|
videoRef.current.pause();
|
||||||
videoRef.current.resume();
|
videoRef.current.resume();
|
||||||
|
|
||||||
// save video to your Photos with current filter prop
|
// Save video
|
||||||
const response = await videoRef.current.save();
|
const response = await videoRef.current.save();
|
||||||
const path = response.uri;
|
console.log('Saved video path:', response.uri);
|
||||||
|
|
||||||
// seek to the specified position represented by seconds
|
// Seek to 200s (or with tolerance on iOS)
|
||||||
videoRef.current.seek(200);
|
videoRef.current.seek(200);
|
||||||
// or on iOS you can seek with tolerance
|
|
||||||
videoRef.current.seek(200, 10);
|
videoRef.current.seek(200, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Video
|
<Video
|
||||||
ref={videoRef}
|
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`
|
### `getWidevineLevel`
|
||||||
|
|
||||||
<PlatformsList types={['Android']} />
|
<PlatformsList types={['Android']} />
|
||||||
|
|
||||||
Indicates whether the widevine level supported by device.
|
```tsx
|
||||||
|
getWidevineLevel(): Promise<number>
|
||||||
|
```
|
||||||
|
|
||||||
Possible values are:
|
Returns the **Widevine DRM level**:
|
||||||
|
|
||||||
- 0 - unable to determine widevine support (typically not supported)
|
- **0** → Unknown / Not supported.
|
||||||
- 1, 2, 3 - Widevine level supported
|
- **1, 2, 3** → Supported Widevine levels.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### `isCodecSupported`
|
### `isCodecSupported`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'web']} />
|
<PlatformsList types={['Android', 'web']} />
|
||||||
|
|
||||||
Indicates whether the provided codec is supported level supported by device.
|
```tsx
|
||||||
|
isCodecSupported(mimetype: string, width: number, height: number): Promise<'hardware' | 'software' | 'unsupported'>
|
||||||
|
```
|
||||||
|
|
||||||
parameters:
|
Checks if the given **video codec** is supported.
|
||||||
|
|
||||||
- `mimetype`: mime type of codec to query
|
| Result | Meaning |
|
||||||
- `width`, `height`: resolution to query
|
|------------|--------------------------------------|
|
||||||
|
| `hardware` | Hardware decoding supported |
|
||||||
|
| `software` | Only software decoding available |
|
||||||
|
| `unsupported` | Codec **not supported** |
|
||||||
|
|
||||||
Possible results:
|
---
|
||||||
|
|
||||||
- `hardware` - codec is supported by hardware
|
|
||||||
- `software` - codec is supported by software only
|
|
||||||
- `unsupported` - codec is not supported
|
|
||||||
|
|
||||||
### `isHEVCSupported`
|
### `isHEVCSupported`
|
||||||
|
|
||||||
<PlatformsList types={['Android']} />
|
<PlatformsList types={['Android']} />
|
||||||
|
|
||||||
Helper which Indicates whether the provided HEVC/1920\*1080 is supported level supported by device. It uses isCodecSupported internally.
|
```tsx
|
||||||
|
isHEVCSupported(): Promise<boolean>
|
||||||
|
```
|
||||||
|
|
||||||
### Example Usage
|
Checks if **HEVC (H.265)** is supported at **1920×1080 resolution**.
|
||||||
|
|
||||||
|
> Uses `isCodecSupported` internally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Static Methods Example Usage
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { VideoDecoderProperties } from 'react-native-video';
|
import { VideoDecoderProperties } from 'react-native-video';
|
||||||
|
|
||||||
VideoDecoderProperties.getWidevineLevel().then((level) => {
|
VideoDecoderProperties.getWidevineLevel().then((level) => {
|
||||||
...
|
console.log('Widevine Level:', level);
|
||||||
});
|
});
|
||||||
|
|
||||||
VideoDecoderProperties.isCodecSupported('video/hevc', 1920, 1080).then((support) => {
|
VideoDecoderProperties.isCodecSupported('video/hevc', 1920, 1080).then((support) => {
|
||||||
...
|
console.log('HEVC Support:', support);
|
||||||
});
|
});
|
||||||
|
|
||||||
VideoDecoderProperties.isHEVCSupported().then((support) => {
|
VideoDecoderProperties.isHEVCSupported().then((support) => {
|
||||||
...
|
console.log('HEVC 1080p Support:', support);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,64 +1,63 @@
|
|||||||
# A `<Video>` component for React Native
|
# A `<Video>` Component for React Native
|
||||||
|
|
||||||
## About
|
## About
|
||||||
`react-native-video` is a React Native library that provides a Video component that renders media content such as videos and streams
|
`react-native-video` is a React Native library that provides a Video component to render media content like videos and streams.
|
||||||
|
|
||||||
It allows to stream video files (m3u, mpd, mp4, ...) inside your react native application.
|
It allows you to stream video files (m3u, mpd, mp4, etc.) inside your React Native application.
|
||||||
|
|
||||||
- Exoplayer for android
|
- ExoPlayer for Android
|
||||||
- AVplayer for iOS, tvOS and visionOS
|
- AVPlayer for iOS, tvOS, and visionOS
|
||||||
- Windows UWP for windows
|
- Windows UWP for Windows
|
||||||
- HTML5 for web
|
- HTML5 for Web
|
||||||
- Trick mode support
|
- Trick mode support
|
||||||
- Subtitles (embeded or side loaded)
|
- Subtitles (embedded or side-loaded)
|
||||||
- DRM support
|
- DRM support
|
||||||
- Client side Ads insertion (via google IMA)
|
- Client-side ad insertion (via Google IMA)
|
||||||
- Pip (ios)
|
- PiP (Picture-in-Picture)
|
||||||
- Embedded playback controls
|
- Embedded playback controls
|
||||||
- And much more
|
- And more
|
||||||
|
|
||||||
The aim of this package is to have a thin and exhaustive control of player.
|
The goal of this package is to provide lightweight but full control over the player.
|
||||||
|
|
||||||
## V6.0.0 Information
|
## V6.0.0 Information
|
||||||
> ⚠️ **Version 6**: The following documentation refer to features only available through the v6.0.0 releases.
|
> ⚠️ **Version 6**: This documentation covers features available only in v6.0.0 and later.
|
||||||
> 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)
|
> If you're unsure or need an older version, you can still use [version 5.2.x](https://github.com/TheWidlarzGroup/react-native-video/blob/v5.2.0/README.md).
|
||||||
|
|
||||||
Version 6.x requires **react-native >= 0.68.2**
|
Version 6.x requires **react-native >= 0.68.2**
|
||||||
> ⚠️ from **6.0.0-beta.8** requires also **iOS >= 13.0** (default in react-native 0.73)
|
> ⚠️ From **6.0.0-beta.8**, it also requires **iOS >= 13.0** (default in React Native 0.73).
|
||||||
|
|
||||||
For older versions of react-native, [please use version 5.x](https://github.com/TheWidlarzGroup/react-native-video/tree/v5.2.0).
|
For older versions of React Native, [please use version 5.x](https://github.com/TheWidlarzGroup/react-native-video/tree/v5.2.0).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Load the module
|
// Load the module
|
||||||
|
import Video, { VideoRef } from 'react-native-video';
|
||||||
|
|
||||||
import Video, {VideoRef} from 'react-native-video';
|
// Inside your render function, assuming you have a file called
|
||||||
|
|
||||||
// Within your render function, assuming you have a file called
|
|
||||||
// "background.mp4" in your project. You can include multiple videos
|
// "background.mp4" in your project. You can include multiple videos
|
||||||
// on a single screen if you like.
|
// on a single screen if needed.
|
||||||
|
|
||||||
const VideoPlayer = () => {
|
const VideoPlayer = () => {
|
||||||
const videoRef = useRef<VideoRef>(null);
|
const videoRef = useRef<VideoRef>(null);
|
||||||
const background = require('./background.mp4');
|
const background = require('./background.mp4');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Video
|
<Video
|
||||||
// Can be a URL or a local file.
|
// Can be a URL or a local file.
|
||||||
source={background}
|
source={background}
|
||||||
// Store reference
|
// Store reference
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
// Callback when remote video is buffering
|
// Callback when remote video is buffering
|
||||||
onBuffer={onBuffer}
|
onBuffer={onBuffer}
|
||||||
// Callback when video cannot be loaded
|
// Callback when the video cannot be loaded
|
||||||
onError={onError}
|
onError={onError}
|
||||||
style={styles.backgroundVideo}
|
style={styles.backgroundVideo}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Later on in your styles..
|
// Later in your styles...
|
||||||
var styles = StyleSheet.create({
|
var styles = StyleSheet.create({
|
||||||
backgroundVideo: {
|
backgroundVideo: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -69,3 +68,4 @@ var styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
Using npm:
|
Using npm:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@@ -13,76 +12,99 @@ or using yarn:
|
|||||||
yarn add react-native-video
|
yarn add react-native-video
|
||||||
```
|
```
|
||||||
|
|
||||||
Then follow the instructions for your platform to link react-native-video into your project
|
Then follow the instructions for your platform to link `react-native-video` into your project.
|
||||||
|
|
||||||
|
# Specific Platform Installation
|
||||||
|
|
||||||
# Specific platform installation
|
|
||||||
<details>
|
<details>
|
||||||
<summary>iOS</summary>
|
<summary>iOS</summary>
|
||||||
|
|
||||||
## iOS
|
## iOS
|
||||||
|
|
||||||
### Standard Method
|
### Standard Method
|
||||||
|
|
||||||
Run `pod install` in the `ios` directory of your project.
|
Run `pod install` in the `ios` directory of your project.
|
||||||
|
|
||||||
⚠️ from version `6.0.0` the minimum iOS version required is `13.0`. For more information see [updating section](updating.md)
|
⚠️ From version `6.0.0`, the minimum iOS version required is `13.0`. For more information, see the [updating section](updating.md).
|
||||||
|
|
||||||
### Enable custom feature in podfile file
|
### Enable Custom Features in the Podfile
|
||||||
|
|
||||||
Samples available in sample app see [sample pod file](https://github.com/TheWidlarzGroup/react-native-video/blob/9c669a2d8a53df36773fd82ff0917280d0659bc7/examples/basic/ios/Podfile#L34)
|
Sample configurations are available in the sample app. See the [sample pod file](https://github.com/TheWidlarzGroup/react-native-video/blob/9c669a2d8a53df36773fd82ff0917280d0659bc7/examples/basic/ios/Podfile#L34).
|
||||||
|
|
||||||
#### Video caching
|
#### Video Caching
|
||||||
|
|
||||||
To enable Video caching usage, add following line in your podfile:
|
To enable video caching, add the following line to your Podfile: ([more info here](other/caching.md))
|
||||||
([more info here](other/caching.md))
|
|
||||||
|
|
||||||
```podfile
|
```podfile
|
||||||
# enable Video caching
|
# Enable Video Caching
|
||||||
+ $RNVideoUseVideoCaching=true
|
$RNVideoUseVideoCaching=true
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Google IMA
|
#### Google IMA
|
||||||
|
|
||||||
Google IMA is the google SDK to support Client Side Ads Integration (CSAI), see [google documentation](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side) for more information.
|
Google IMA is the SDK for client-side ads integration. See the [Google documentation](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side) for more details.
|
||||||
|
|
||||||
|
To enable Google IMA, add the following line to your Podfile:
|
||||||
|
|
||||||
To enable google IMA usage define add following line in your podfile:
|
|
||||||
```podfile
|
```podfile
|
||||||
$RNVideoUseGoogleIMA=true
|
$RNVideoUseGoogleIMA=true
|
||||||
```
|
```
|
||||||
|
|
||||||
**If you are using Expo you can use [expo plugin](other/expo.md) for it**
|
**If you are using Expo, you can use the [Expo plugin](other/expo.md).**
|
||||||
|
|
||||||
|
> **Note:** If you are enabling video caching (using `$RNVideoUseVideoCaching`), you must add the following to your `Gemfile`:
|
||||||
|
>
|
||||||
|
> ```ruby
|
||||||
|
> gem "cocoapods-swift-modular-headers"
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> Then, install dependencies using:
|
||||||
|
>
|
||||||
|
> ```sh
|
||||||
|
> bundle install
|
||||||
|
> bundle exec pod install
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> This enables Swift modular headers for Swift dependencies.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Android</summary>
|
<summary>Android</summary>
|
||||||
|
|
||||||
## Android
|
## Android
|
||||||
|
|
||||||
From version >= 6.0.0, your application needs to have kotlin version >= 1.8.0
|
From version `>= 6.0.0`, your application must use Kotlin version `>= 1.8.0`.
|
||||||
|
|
||||||
```:
|
```gradle
|
||||||
buildscript {
|
buildscript {
|
||||||
...
|
...
|
||||||
ext.kotlinVersion = '1.8.0',
|
ext.kotlinVersion = '1.8.0'
|
||||||
ext.compileSdkVersion = 34
|
ext.compileSdkVersion = 34
|
||||||
ext.targetSdkVersion = 34
|
ext.targetSdkVersion = 34
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Enable custom feature in gradle file
|
### Enable Custom Features in the Gradle File
|
||||||
|
|
||||||
**If you are using Expo you can use [expo plugin](other/expo.md) for it**
|
**If you are using Expo, you can use the [Expo plugin](other/expo.md).**
|
||||||
|
|
||||||
You can disable or enable the following features by setting the following variables in your `android/build.gradle` file:
|
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)
|
|
||||||
|
- `useExoplayerIMA` - Enable Google IMA SDK (ads support)
|
||||||
- `useExoplayerRtsp` - Enable RTSP support
|
- `useExoplayerRtsp` - Enable RTSP support
|
||||||
- `useExoplayerSmoothStreaming` - Enable SmoothStreaming support
|
- `useExoplayerSmoothStreaming` - Enable SmoothStreaming support
|
||||||
- `useExoplayerDash` - Enable Dash support
|
- `useExoplayerDash` - Enable Dash support
|
||||||
- `useExoplayerHls` - Enable HLS support
|
- `useExoplayerHls` - Enable HLS support
|
||||||
|
|
||||||
Each of these features enabled will increase the size of your APK, so only enable the features you need.
|
Each enabled feature increases the APK size, so only enable what you need.
|
||||||
By default enabled features are: `useExoplayerSmoothStreaming`, `useExoplayerDash`, `useExoplayerHls`
|
|
||||||
|
|
||||||
|
By default, the enabled features are:
|
||||||
|
|
||||||
|
- `useExoplayerSmoothStreaming`
|
||||||
|
- `useExoplayerDash`
|
||||||
|
- `useExoplayerHls`
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -100,9 +122,10 @@ buildscript {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
See [sample app](https://github.com/TheWidlarzGroup/react-native-video/blob/9c669a2d8a53df36773fd82ff0917280d0659bc7/examples/basic/android/build.gradle#L14C5-L14C5)
|
See the [sample app](https://github.com/TheWidlarzGroup/react-native-video/blob/9c669a2d8a53df36773fd82ff0917280d0659bc7/examples/basic/android/build.gradle#L14C5-L14C5).
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Windows</summary>
|
<summary>Windows</summary>
|
||||||
|
|
||||||
@@ -112,81 +135,99 @@ See [sample app](https://github.com/TheWidlarzGroup/react-native-video/blob/9c66
|
|||||||
|
|
||||||
**React Native Windows 0.63 and above**
|
**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
|
### Manual Linking
|
||||||
|
|
||||||
**React Native Windows 0.62**
|
**React Native Windows 0.62**
|
||||||
|
|
||||||
Make the following additions to the given files manually:
|
Make the following manual additions:
|
||||||
|
|
||||||
`windows\myapp.sln`
|
#### `windows\myapp.sln`
|
||||||
|
|
||||||
Add the _ReactNativeVideoCPP_ project to your solution (eg. `windows\myapp.sln`):
|
Add the _ReactNativeVideoCPP_ project to your solution:
|
||||||
|
|
||||||
1. Open your solution in Visual Studio 2019
|
1. Open your solution in Visual Studio 2019.
|
||||||
2. Right-click Solution icon in Solution Explorer > Add > Existing Project...
|
2. Right-click the Solution icon in Solution Explorer > Add > Existing Project...
|
||||||
3. Select `node_modules\react-native-video\windows\ReactNativeVideoCPP\ReactNativeVideoCPP.vcxproj`
|
3. Select `node_modules\react-native-video\windows\ReactNativeVideoCPP\ReactNativeVideoCPP.vcxproj`.
|
||||||
|
|
||||||
`windows\myapp\myapp.vcxproj`
|
#### `windows\myapp\myapp.vcxproj`
|
||||||
|
|
||||||
Add a reference to _ReactNativeVideoCPP_ to your main application project (eg. `windows\myapp\myapp.vcxproj`):
|
Add a reference to _ReactNativeVideoCPP_ to your main application project:
|
||||||
|
|
||||||
1. Open your solution in Visual Studio 2019
|
1. Open your solution in Visual Studio 2019.
|
||||||
2. Right-click main application project > Add > Reference...
|
2. Right-click the main application project > Add > Reference...
|
||||||
3. Check _ReactNativeVideoCPP_ from Solution Projects
|
3. Check _ReactNativeVideoCPP_ from Solution Projects.
|
||||||
|
|
||||||
`pch.h`
|
#### `pch.h`
|
||||||
|
|
||||||
Add `#include "winrt/ReactNativeVideoCPP.h"`.
|
Add:
|
||||||
|
|
||||||
`app.cpp`
|
```cpp
|
||||||
|
#include "winrt/ReactNativeVideoCPP.h"
|
||||||
|
```
|
||||||
|
|
||||||
Add `PackageProviders().Append(winrt::ReactNativeVideoCPP::ReactPackageProvider());` before `InitializeComponent();`.
|
#### `app.cpp`
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
PackageProviders().Append(winrt::ReactNativeVideoCPP::ReactPackageProvider());
|
||||||
|
```
|
||||||
|
|
||||||
|
before `InitializeComponent();`.
|
||||||
|
|
||||||
**React Native Windows 0.61 and below**
|
**React Native Windows 0.61 and below**
|
||||||
|
|
||||||
Follow the manual linking instructions for React Native Windows 0.62 above, but substitute _ReactNativeVideoCPP61_ for _ReactNativeVideoCPP_.
|
Follow the manual linking steps for React Native Windows 0.62, but use _ReactNativeVideoCPP61_ instead of _ReactNativeVideoCPP_.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>tvOS</summary>
|
<summary>tvOS</summary>
|
||||||
|
|
||||||
## tvOS
|
## tvOS
|
||||||
|
|
||||||
`react-native link react-native-video` doesn’t work properly with the tvOS target so we need to add the library manually.
|
`react-native link react-native-video` does not work properly with the tvOS target, so the library must be added manually.
|
||||||
|
|
||||||
First select your project in Xcode.
|
### Steps:
|
||||||
|
|
||||||

|
1. Select your project in Xcode.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
Scroll to « Linked Frameworks and Libraries » and tap on the + button
|

|
||||||
|
|
||||||

|
3. Scroll to "Linked Frameworks and Libraries" and click the `+` button.
|
||||||
|
|
||||||
Select RCTVideo-tvOS
|

|
||||||
|
|
||||||
|
4. Select `RCTVideo-tvOS`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||

|
|
||||||
</details>
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>visionOS</summary>
|
<summary>visionOS</summary>
|
||||||
|
|
||||||
## visionOS
|
## visionOS
|
||||||
Run `pod install` in the `visionos` directory of your project
|
|
||||||
|
Run `pod install` in the `visionos` directory of your project.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>web</summary>
|
<summary>Web</summary>
|
||||||
|
|
||||||
Nothing to do, everything should work out of the box.
|
## Web
|
||||||
|
|
||||||
Note that only basic video support is present, no hls/dash or ads/drm for now.
|
No additional setup is required.
|
||||||
|
|
||||||
|
The Railbird fork uses Shaka Player for browser playback, including HLS and DASH
|
||||||
|
sources. The native ads and DRM props are not currently integrated with the web
|
||||||
|
player.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"caching": "Caching",
|
"caching": "Caching",
|
||||||
|
"downloading": "Downloading",
|
||||||
"misc": "Misc",
|
"misc": "Misc",
|
||||||
"debug": "Debugging",
|
"debug": "Debugging",
|
||||||
"new-arch": "New Architecture",
|
"new-arch": "New Architecture",
|
||||||
"expo": "Expo",
|
"expo": "Expo",
|
||||||
"plugin": "Plugin (experimental)"
|
"plugin": "Plugin (experimental)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
# Caching
|
# 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
|
||||||
|
|
||||||
Android uses a LRU `SimpleCache` with a variable cache size that can be specified by bufferConfig - cacheSizeMB. This creates a folder named `RNVCache` in the app's `cache` folder. Do note RNV does not yet offer a native call to flush the cache, it can be flushed by clearing the app's cache.
|
Android uses an LRU `SimpleCache` with a variable cache size, which can be specified by `bufferConfig - cacheSizeMB`. This creates a folder named `RNVCache` inside the app's `cache` directory.
|
||||||
|
|
||||||
In addition, this resolves RNV6's repeated source URI call problem when looping a video on Android.
|
Note that `react-native-video` does not currently offer a native method to flush the cache, but it can be cleared by manually clearing the app's cache.
|
||||||
|
|
||||||
|
Additionally, this resolves the issue in RNV6 where the source URI was repeatedly called when looping a video on Android.
|
||||||
|
|
||||||
## iOS
|
## iOS
|
||||||
|
|
||||||
@@ -14,17 +16,33 @@ In addition, this resolves RNV6's repeated source URI call problem when looping
|
|||||||
|
|
||||||
The cache is backed by [SPTPersistentCache](https://github.com/spotify/SPTPersistentCache) and [DVAssetLoaderDelegate](https://github.com/vdugnist/DVAssetLoaderDelegate).
|
The cache is backed by [SPTPersistentCache](https://github.com/spotify/SPTPersistentCache) and [DVAssetLoaderDelegate](https://github.com/vdugnist/DVAssetLoaderDelegate).
|
||||||
|
|
||||||
### How Does It Work
|
### How It Works
|
||||||
|
|
||||||
The caching is based on the url of the asset.
|
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.
|
||||||
SPTPersistentCache is a LRU ([Least Recently Used](<https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)>)) cache.
|
|
||||||
|
|
||||||
### Restrictions
|
### Restrictions
|
||||||
|
|
||||||
Currently, caching is only supported for URLs that end in a `.mp4`, `.m4v`, or `.mov` extension. In future versions, URLs that end in a query string (e.g. test.mp4?resolution=480p) will be support once dependencies allow access to the `Content-Type` header. At this time, HLS playlists (.m3u8) and videos that sideload text tracks are not supported and will bypass the cache.
|
Currently, caching is only supported for URLs ending in `.mp4`, `.m4v`, or `.mov`. In future versions, URLs with query strings (e.g., `test.mp4?resolution=480p`) will be supported once dependencies allow access to the `Content-Type` header.
|
||||||
|
|
||||||
You will also receive warnings in the Xcode logs by using the `debug` mode. So if you are not 100% sure if your video is cached, check your Xcode logs!
|
At this time, HLS playlists (`.m3u8`) and videos with sideloaded text tracks are not supported and will bypass the cache.
|
||||||
|
|
||||||
By default files expire after 30 days and the maximum cache size is 100mb.
|
You will see warnings in the Xcode logs when using `debug` mode. If you're unsure whether your video is cached, check your Xcode logs.
|
||||||
|
|
||||||
In a future release the cache might have more configurable options.
|
By default, files expire after 30 days, and the maximum cache size is 100MB.
|
||||||
|
|
||||||
|
Future updates may include more configurable caching options.
|
||||||
|
|
||||||
|
> **Note:** If you are enabling video caching (using `$RNVideoUseVideoCaching`), you must add the following to your `Gemfile`:
|
||||||
|
>
|
||||||
|
> ```ruby
|
||||||
|
> gem "cocoapods-swift-modular-headers"
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> Then, install dependencies using:
|
||||||
|
>
|
||||||
|
> ```sh
|
||||||
|
> bundle install
|
||||||
|
> bundle exec pod install
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> This enables Swift modular headers for Swift dependencies.
|
||||||
|
|||||||
@@ -1,20 +1,38 @@
|
|||||||
# Debugging
|
# Debugging
|
||||||
|
|
||||||
This page describe useful tips for debugging and investigating issue in the package or in your application.
|
This page provides useful tips for debugging and troubleshooting issues in the package or your application.
|
||||||
|
|
||||||
## Using the sample app
|
## 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.
|
||||||
|
|
||||||
## HTTP playback doesn't work or Black Screen on Release build (Android)
|
To use the sample app, follow these steps:
|
||||||
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)
|
|
||||||
|
- Clone this repository:
|
||||||
|
```shell
|
||||||
|
git clone git@github.com:TheWidlarzGroup/react-native-video.git
|
||||||
|
```
|
||||||
|
- Navigate to the root folder and build the package. This generates a transpiled version in the `lib` folder:
|
||||||
|
```shell
|
||||||
|
cd react-native-video && yarn && yarn build
|
||||||
|
```
|
||||||
|
- Navigate to the sample app and install dependencies:
|
||||||
|
```shell
|
||||||
|
cd example/basic && yarn install
|
||||||
|
```
|
||||||
|
- Build and run the app:
|
||||||
|
- For Android:
|
||||||
|
```shell
|
||||||
|
yarn android
|
||||||
|
```
|
||||||
|
- For iOS:
|
||||||
|
```shell
|
||||||
|
cd ios && pod install && cd .. && yarn ios
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP Playback Doesn't Work or Black Screen on Release Build (Android)
|
||||||
|
|
||||||
|
If your video works in Debug mode but shows only a black screen in Release mode, check the URL of your video. If you are using the `http` protocol, you need to add the following line to your `AndroidManifest.xml` file. [More details here](https://developer.android.com/guide/topics/manifest/application-element#usesCleartextTraffic):
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<application
|
<application
|
||||||
@@ -25,56 +43,56 @@ If your video work on Debug mode, but on Release you see only black screen, plea
|
|||||||
|
|
||||||
## Decoder Issue (Android)
|
## Decoder Issue (Android)
|
||||||
|
|
||||||
Devices have a maximum of simultaneous possible playback. It means you have reach this limit. Exoplayer returns: 'Unable to instantiate decoder'
|
Some devices have a maximum number of simultaneous video playbacks. If this limit is reached, ExoPlayer returns an error: `Unable to instantiate decoder`.
|
||||||
|
|
||||||
**known issue**: This issue happen really often in debug mode.
|
**Known issue:** This happens frequently in Debug mode.
|
||||||
|
|
||||||
## You cannot play clean content (all OS)
|
## Unable to Play Clear Content (All OS)
|
||||||
|
|
||||||
Here are the steps to consider before opening a ticket in issue tracker
|
Before opening a ticket, follow these steps:
|
||||||
|
|
||||||
## Check you can access to remote file
|
### Check Remote File Access
|
||||||
|
|
||||||
Ensure you can download to manifest / content file with a browser for example
|
Ensure you can download the manifest/content file using a browser.
|
||||||
|
|
||||||
## Check another player can read the content
|
### Check If Another Player Can Play the Content
|
||||||
|
|
||||||
Usually clear playback can be read with all Video player. Then you should ensure content can be played without any issue with another player ([VideoLan/VLC](https://www.videolan.org/vlc/) is a good reference implementation)
|
Clear playback should work with any video player. Test the content with another player, such as [VLC](https://www.videolan.org/vlc/), to confirm it plays without issues.
|
||||||
|
|
||||||
## You cannot play protected content (all OS)
|
## Unable to Play Protected Content (All OS)
|
||||||
|
|
||||||
## Protected content gives error (token error / access forbidden)
|
### Protected Content Gives an Error (Token Error / Access Forbidden)
|
||||||
|
|
||||||
If content is protected with an access token or any other http header, ensure you can access to you data with a wget call or a rest client app. You need to provide all needed access token / authentication parameters.
|
If the content requires an access token or HTTP headers, ensure you can access the data using `wget` or a REST client. Provide all necessary authentication parameters.
|
||||||
|
|
||||||
## I need to debug network calls but I don't see them in react native debugging tools
|
## Debugging Network Calls Not Visible in React Native Debugging Tools
|
||||||
|
|
||||||
This is a react native limitation. React native tools can only see network calls done in JS.
|
This is a React Native limitation—React Native debugging tools only capture network calls made in JavaScript.
|
||||||
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, ...
|
|
||||||
|
|
||||||
Then try to compare exchanges with previous tests you made.
|
To debug network calls, use tools like:
|
||||||
|
- [Charles Proxy](https://www.charlesproxy.com/)
|
||||||
|
- [Fiddler](https://www.telerik.com/fiddler)
|
||||||
|
|
||||||
## Debug media3: build from media3 source
|
These tools allow you to sniff all HTTP/HTTPS calls, including access to content, DRM, and audio/video chunks. Compare the request/response patterns with previous tests to diagnose issues.
|
||||||
|
|
||||||
If you need to use a specific exoplayer version or patch default behavior, you may want to build from media3 source code.
|
## Debugging Media3: Build from Media3 Source
|
||||||
|
|
||||||
Building from media3 source is possible. You need to add 2 or 3 things in your app:
|
If you need to use a specific ExoPlayer version or modify default behavior, you may need to build from the Media3 source code.
|
||||||
|
|
||||||
### Configure player path
|
### Configure Player Path
|
||||||
|
|
||||||
You need to add following lines in settings.gradle to configure your media3 source path:
|
Add the following lines to `settings.gradle` to configure your Media3 source path:
|
||||||
|
|
||||||
```gradle
|
```gradle
|
||||||
gradle.ext.androidxMediaModulePrefix = 'media-'
|
gradle.ext.androidxMediaModulePrefix = 'media-'
|
||||||
apply from: file("../../../../media3/core_settings.gradle")
|
apply from: file("../../../../media3/core_settings.gradle")
|
||||||
````
|
```
|
||||||
|
|
||||||
Of course, you should replace with media3 source path. Be carefull, you need to use the same version (or version with compatible api) that the package support.
|
Replace this with the actual Media3 source path. Ensure that you use the same version (or a compatible API version) supported by the package.
|
||||||
|
|
||||||
### Enable building from source
|
### Enable Building from Source
|
||||||
In your build.gradle file, add following setting:
|
|
||||||
|
In your `build.gradle` file, add the following setting:
|
||||||
|
|
||||||
```gradle
|
```gradle
|
||||||
buildscript {
|
buildscript {
|
||||||
@@ -86,6 +104,7 @@ buildscript {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## It's still not working
|
## Still Not Working?
|
||||||
|
|
||||||
|
You can open a ticket or contact us for [premium support](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs&utm_campaign=debugging&utm_id=enterprise#Contact).
|
||||||
|
|
||||||
You can try to open a ticket or contact us for [premium support](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs#Contact)!
|
|
||||||
|
|||||||
62
docs/pages/other/downloading.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Offline Video SDK
|
||||||
|
|
||||||
|
## Add Offline Playback to Your React Native App — Fast
|
||||||
|
|
||||||
|
The [Offline Video SDK](https://sdk.thewidlarzgroup.com/offline-video/?utm_source=rnv&utm_medium=docs&utm_campaign=downloading&utm_id=offline-video-sdk-link) is a commercial add-on for `react-native-video` (v6 and v7) that enables secure **offline playback** of HLS streams — including support for **DRM**, **multi-audio**, and **subtitles**.
|
||||||
|
|
||||||
|
It’s 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 — doesn’t 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)
|
||||||
@@ -1,32 +1,32 @@
|
|||||||
# Expo
|
# Expo
|
||||||
|
|
||||||
## Expo plugin
|
## 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.
|
Starting from version `6.3.1`, `react-native-video` supports an Expo plugin. You can configure `react-native-video` properties in the `app.json`, `app.config.json`, or `app.config.js` file.
|
||||||
|
|
||||||
|
This is particularly useful when using the `Expo` managed workflow (`expo prebuild`), as it automatically sets up `react-native-video` properties in the native part of the Expo project.
|
||||||
|
|
||||||
|
### Example Configuration
|
||||||
|
|
||||||
```json
|
```json
|
||||||
// app.json
|
// app.json
|
||||||
{
|
{
|
||||||
{
|
|
||||||
"name": "my app",
|
"name": "my app",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
[
|
[
|
||||||
"react-native-video",
|
"react-native-video",
|
||||||
{
|
{
|
||||||
// ...
|
|
||||||
"enableNotificationControls": true,
|
"enableNotificationControls": true,
|
||||||
"androidExtensions": {
|
"androidExtensions": {
|
||||||
"useExoplayerRtsp": false,
|
"useExoplayerRtsp": false,
|
||||||
"useExoplayerSmoothStreaming": false,
|
"useExoplayerSmoothStreaming": false,
|
||||||
"useExoplayerHls": false,
|
"useExoplayerHls": false,
|
||||||
"useExoplayerDash": false,
|
"useExoplayerDash": false
|
||||||
}
|
}
|
||||||
// ...
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Expo Plugin Properties
|
## Expo Plugin Properties
|
||||||
@@ -37,4 +37,5 @@ It's useful when you are using `expo` managed workflow (expo prebuild) as it wil
|
|||||||
| enableBackgroundAudio | boolean | false | Add required changes to play video in background on iOS |
|
| enableBackgroundAudio | boolean | false | Add required changes to play video in background on iOS |
|
||||||
| enableADSExtension | boolean | false | Add required changes to use ads extension for video player |
|
| 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 |
|
| 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 |
|
| 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 |
|
||||||
|
|||||||
@@ -2,54 +2,60 @@
|
|||||||
|
|
||||||
## iOS App Transport Security
|
## iOS App Transport Security
|
||||||
|
|
||||||
- By default, iOS will only load encrypted (https) urls. If you want to load content from an unencrypted (http) source, you will need to modify your Info.plist file and add the following entry:
|
By default, iOS only allows loading encrypted (`https`) URLs. If you need to load content from an unencrypted (`http`) source, you must modify your `Info.plist` file and add the following entry:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
For more detailed info check this [article](https://cocoacasts.com/how-to-add-app-transport-security-exception-domains)
|
For more details, check this [article](https://cocoacasts.com/how-to-add-app-transport-security-exception-domains).
|
||||||
</details>
|
|
||||||
|
|
||||||
## Audio Mixing
|
## Audio Mixing
|
||||||
|
|
||||||
At some point in the future, react-native-video will include an Audio Manager for configuring how videos mix with other apps playing sounds on the device.
|
In future versions, `react-native-video` will include an Audio Manager for configuring how videos mix with other audio-playing apps.
|
||||||
|
|
||||||
On iOS, if you would like to allow other apps to play music over your video component, make the following change:
|
On iOS, if you want to allow background music from other apps to continue playing over your video component, update your `AppDelegate.m` file:
|
||||||
|
|
||||||
**AppDelegate.m**
|
### **AppDelegate.m**
|
||||||
|
|
||||||
```objective-c
|
```objective-c
|
||||||
#import <AVFoundation/AVFoundation.h> // import
|
#import <AVFoundation/AVFoundation.h> // Import the AVFoundation framework
|
||||||
|
|
||||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||||
{
|
{
|
||||||
...
|
...
|
||||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil]; // allow
|
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil]; // Allow background audio
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also use the [ignoreSilentSwitch](#ignoresilentswitch) prop.
|
You can also use the [`ignoreSilentSwitch`](#ignoresilentswitch) prop.
|
||||||
</details>
|
|
||||||
|
|
||||||
## Android Expansion File Usage
|
## Android Expansion File Usage
|
||||||
Expansions files allow you to ship assets that exceed the 100MB apk size limit and don't need to be updated each time you push an app update.
|
|
||||||
|
|
||||||
This only supports mp4 files and they must not be compressed. Example command line for preventing compression:
|
Expansion files allow you to include assets exceeding the 100MB APK size limit without requiring an update every time you push a new version.
|
||||||
|
|
||||||
|
- Only supports `.mp4` files, and they **must not be compressed**.
|
||||||
|
- Example command to prevent compression:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
zip -r -n .mp4 *.mp4 player.video.example.com
|
zip -r -n .mp4 *.mp4 player.video.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Example Usage in Code:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Within your render function, assuming you have a file called
|
// Assuming "background.mp4" is included in your expansion file.
|
||||||
// "background.mp4" in your expansion file. Just add your main and (if applicable) patch version
|
<Video
|
||||||
<Video source={{uri: "background", mainVer: 1, patchVer: 0}} // Looks for .mp4 file (background.mp4) in the given expansion version.
|
source={{uri: "background", mainVer: 1, patchVer: 0}} // Looks for "background.mp4" in the specified expansion version.
|
||||||
resizeMode="cover" // Fill the whole screen at aspect ratio.
|
resizeMode="cover" // Fill the whole screen while maintaining aspect ratio.
|
||||||
style={styles.backgroundVideo} />
|
style={styles.backgroundVideo}
|
||||||
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Load files with the RN Asset System
|
## Load Files with the React Native Asset System
|
||||||
|
|
||||||
The asset system [introduced in RN `0.14`](http://www.reactnative.com/react-native-v0-14-0-released/) allows loading image resources shared across iOS and Android without touching native code. As of RN `0.31` [the same is true](https://github.com/facebook/react-native/commit/91ff6868a554c4930fd5fda6ba8044dbd56c8374) of mp4 video assets for Android. As of [RN `0.33`](https://github.com/facebook/react-native/releases/tag/v0.33.0) iOS is also supported. Requires `react-native-video@0.9.0`.
|
The asset system introduced in RN `0.14` allows loading shared image resources across iOS and Android without modifying native code. As of RN `0.31`, the same applies to `.mp4` video assets on Android. From RN `0.33`, iOS support was added. Requires `react-native-video@0.9.0` or later.
|
||||||
|
|
||||||
|
### Example:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
<Video
|
<Video
|
||||||
@@ -57,6 +63,8 @@ The asset system [introduced in RN `0.14`](http://www.reactnative.com/react-nati
|
|||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Play in background on iOS
|
## Play in Background on iOS
|
||||||
|
|
||||||
To enable audio to play in background on iOS the audio session needs to be set to `AVAudioSessionCategoryPlayback`. See [Apple documentation][3] for additional details. (NOTE: there is now a ticket to [expose this as a prop]( https://github.com/react-native-community/react-native-video/issues/310) )
|
To allow audio playback in the background on iOS, set the audio session to `AVAudioSessionCategoryPlayback`. See the [Apple documentation](https://developer.apple.com/documentation/avfoundation/avaudiosession) for more details.
|
||||||
|
|
||||||
|
_(Note: There is an open ticket to [expose this as a prop](https://github.com/react-native-community/react-native-video/issues/310).)_
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
# New Architecture
|
# New Architecture
|
||||||
|
|
||||||
## Fabric
|
## Fabric
|
||||||
Library currently does not support Fabric. We are working on it. In the meantime, you can use Interop Layer.
|
|
||||||
|
The library currently does not support Fabric. We are working on adding support. In the meantime, you can use the Interop Layer.
|
||||||
|
|
||||||
## Interop Layer
|
## Interop Layer
|
||||||
You can use this library on New Architecture by using Interop Layer. <br/> To use Interop Layer you need to have `react-native` >= `0.72.0` & `react-native-video` >= `6.0.0-beta.5`.
|
|
||||||
|
|
||||||
For `react-native` < `0.74` you need to add config in `react-native.config.js` file.
|
You can use this library with the New Architecture by enabling the Interop Layer.
|
||||||
|
|
||||||
|
### Requirements:
|
||||||
|
- `react-native` **>= 0.72.0**
|
||||||
|
- `react-native-video` **>= 6.0.0-beta.5**
|
||||||
|
|
||||||
|
For `react-native` versions **< 0.74**, you need to add the following configuration in the `react-native.config.js` file:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -22,4 +28,5 @@ module.exports = {
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Bridgeless Mode
|
## Bridgeless Mode
|
||||||
Library currently does not support Bridgeless Mode. We are working on it.
|
|
||||||
|
The library currently does not support Bridgeless Mode. We are working on adding support.
|
||||||
|
|||||||
@@ -1,115 +1,312 @@
|
|||||||
# Plugin (experimental)
|
# Plugin
|
||||||
|
|
||||||
Since Version 6.4.0, it is possible to create plugins for analytics management and maybe much more.
|
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 in: example/react-native-video-plugin-sample. (important FIXME, put sample link)
|
A sample plugin is available in the repository: [example/react-native-video-plugin-sample](https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples/react-native-video-plugin-sample).
|
||||||
|
|
||||||
## Concept
|
## Commercial Plugins
|
||||||
|
|
||||||
Most of the analytics system which tracks player information (bitrate, errors, ...) can be integrated directly with Exoplayer or AVPlayer handles.
|
We at The Widlarz Group have created a set of plugins for comprehensive offline video support. If you are interested, check out our [Offline Video SDK](https://sdk.thewidlarzgroup.com/offline-video/?utm_source=rnv&utm_medium=docs&utm_campaign=plugins&utm_id=text). If you need additional plugins (analytics, processing, etc.), let us know.
|
||||||
|
|
||||||
This plugin system allows none intrusive integration of analytics in the react-native-package. It shall be done in native language (kotlin/swift).
|
> Using or recommending our commercial software helps support the maintenance of this open-source project. Thank you!
|
||||||
|
|
||||||
The idea behind this system is to be able to plug an analytics package to react native video without doing any code change (ideally).
|
## Plugins for Analytics
|
||||||
|
|
||||||
Following documentation will show on how to create a new plugin for react native video
|
Most analytics systems that track player data (e.g., bitrate, errors) can be integrated directly with ExoPlayer or AVPlayer.
|
||||||
|
This plugin system allows for non-intrusive analytics integration with `react-native-video`. It should be implemented in native languages (Kotlin/Swift) to ensure efficiency.
|
||||||
|
|
||||||
## Warning and consideration
|
The goal is to enable easy analytics integration without modifying `react-native-video` itself.
|
||||||
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.
|
|
||||||
|
|
||||||
As api is flexible, it makes possible to missuse the system. It is necessary to consider the player handle as read-only. If you modify player behavior, we cannot garanty the good behavior of react-native-video package.
|
## Warnings & Considerations
|
||||||
|
|
||||||
## General
|
This is an **experimental API** and may change over time. The API is simple yet flexible enough to implement analytics systems.
|
||||||
|
If additional metadata is needed, you should implement a setter in your custom package.
|
||||||
|
|
||||||
First you need to create a new react native package:
|
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`.
|
||||||
````shell
|
|
||||||
|
## General Setup
|
||||||
|
|
||||||
|
First, create a new React Native package:
|
||||||
|
|
||||||
|
```shell
|
||||||
npx create-react-native-library@latest react-native-video-custom-analytics
|
npx create-react-native-library@latest react-native-video-custom-analytics
|
||||||
````
|
```
|
||||||
|
|
||||||
Both android and iOS implementation expose an interface `RNVPlugin`.
|
Both Android and iOS implementations expose an `RNVPlugin` interface.
|
||||||
Your `react-native-video-custom-analytics` shall implement this interface and register itself as a plugin for react native video.
|
Your `react-native-video-custom-analytics` package should implement this interface and register itself as a plugin for `react-native-video`.
|
||||||
|
|
||||||
## Android
|
## Plugin Types
|
||||||
There is no special requierement for gradle file.
|
|
||||||
You need two mandatory action to be able to receive player handle
|
|
||||||
|
|
||||||
### 1/ Create the plugin
|
There are two types of plugins you can implement:
|
||||||
|
|
||||||
First you should instanciate a class which extends `RNVPlugin`.
|
1. **Base Plugin (`RNVPlugin`)**: For general-purpose plugins that don't need specific player implementation details.
|
||||||
|
2. **Player-Specific Plugins**:
|
||||||
|
- `RNVAVPlayerPlugin` for iOS: Provides type-safe access to AVPlayer instances
|
||||||
|
- `RNVExoplayerPlugin` for Android: Provides type-safe access to ExoPlayer instances
|
||||||
|
|
||||||
The proposed integration implement `RNVPlugin` directly inside the Module file (`VideoPluginSampleModule`).
|
Choose the appropriate plugin type based on your needs. If you need direct access to player-specific APIs, use the player-specific plugin classes.
|
||||||
|
|
||||||
The `RNVPlugin` interface only defines 2 functions, see description here under.
|
## Android Implementation
|
||||||
|
|
||||||
|
### 1. Create the Plugin
|
||||||
|
|
||||||
|
You can implement either the base `RNVPlugin` interface or the player-specific `RNVExoplayerPlugin` interface.
|
||||||
|
|
||||||
|
#### Base Plugin
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
/**
|
class MyAnalyticsPlugin : RNVPlugin {
|
||||||
* Function called when a new player is created
|
override fun onInstanceCreated(id: String, player: Any) {
|
||||||
* @param id: a random string identifying the player
|
// Handle player creation
|
||||||
* @param player: the instantiated player reference
|
}
|
||||||
*/
|
|
||||||
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)
|
|
||||||
````
|
|
||||||
|
|
||||||
### 2/ register the plugin
|
override fun onInstanceRemoved(id: String, player: Any) {
|
||||||
|
// Handle player removal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
To register this allocated class in the main react native video package you should call following function:
|
#### 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)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Register the Plugin
|
||||||
|
|
||||||
|
To register the plugin within the main `react-native-video` package, call:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
ReactNativeVideoManager.getInstance().registerPlugin(plugin)
|
ReactNativeVideoManager.getInstance().registerPlugin(plugin)
|
||||||
```
|
```
|
||||||
The proposed integration register the instanciated class in `createNativeModules` entry point.
|
|
||||||
|
|
||||||
Your native module can now track Player updates directly from Player reference and report to backend.
|
In the sample implementation, the plugin is registered in the `createNativeModules` entry point.
|
||||||
|
|
||||||
## ios
|
Once registered, your module can track player updates and report analytics data.
|
||||||
|
|
||||||
### 1/ podspec integration
|
### Extending Core Functionality via Plugins
|
||||||
|
|
||||||
Your new module shall be able to access to react-native-video package, then we must declare it as a dependency of the new module you are creating.
|
In addition to analytics, plugins can also be used to modify or override core behavior of `react-native-video`.
|
||||||
|
|
||||||
```podfile
|
This allows native modules to deeply integrate with the playback system - for example:
|
||||||
s.dependency "react-native-video"
|
- replacing the media source factory,
|
||||||
````
|
- modifying the media item before playback starts (e.g., injecting stream keys),
|
||||||
|
- disabling caching dynamically per source.
|
||||||
|
|
||||||
### 2/ Create the plugin
|
These capabilities are available through the advanced Android plugin interface: `RNVExoplayerPlugin`.
|
||||||
|
|
||||||
First you should instanciate a class which extends `RNVPlugin`.
|
> ⚠️ These extension points are optional — if no plugin provides them, the player behaves exactly as it did before.
|
||||||
|
|
||||||
The proposed integration implement `RNVPlugin` directly inside the entry point of the module file (`VideoPluginSample`).
|
---
|
||||||
|
|
||||||
The `RNVPlugin` interface only defines 2 functions, see description here under.
|
#### Plugin Extension Points (Android)
|
||||||
|
|
||||||
```swift
|
If your plugin implements `RNVExoplayerPlugin`, you can override the following methods:
|
||||||
/**
|
|
||||||
* Function called when a new player is created
|
##### 1. `overrideMediaItemBuilder`
|
||||||
* @param player: the instantiated player reference
|
|
||||||
*/
|
Allows you to modify the `MediaItem.Builder` before it’s used. You can inject stream keys, cache keys, or override URIs.
|
||||||
func onInstanceCreated(player: Any)
|
|
||||||
/**
|
```kotlin
|
||||||
* Function called when a player should be destroyed
|
override fun overrideMediaItemBuilder(
|
||||||
* when this callback is called, the plugin shall free all
|
source: Source,
|
||||||
* resources and release all reference to Player object
|
mediaItemBuilder: MediaItem.Builder
|
||||||
* @param player: the player to release
|
): MediaItem.Builder? {
|
||||||
*/
|
// Return modified builder or null to use default
|
||||||
func onInstanceRemoved(player: Any)
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3/ Register the plugin
|
##### 2. `overrideMediaDataSourceFactory`
|
||||||
|
|
||||||
To register this allocated class in the main react native video package you should register it by calling this function:
|
Lets you replace the data source used by ExoPlayer. Useful for implementing read-only cache or request interception.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
override fun overrideMediaDataSourceFactory(
|
||||||
|
source: Source,
|
||||||
|
mediaDataSourceFactory: DataSource.Factory
|
||||||
|
): DataSource.Factory? {
|
||||||
|
// Return your custom factory or null to use default
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 3. `overrideMediaSourceFactory`
|
||||||
|
|
||||||
|
Allows you to override the default MediaSource.Factory used by ExoPlayer for creating media sources.
|
||||||
|
Use this if you need to inject a custom media source implementation. If you return null, the default media source factory will be used.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
override fun overrideMediaSourceFactory(
|
||||||
|
source: Source,
|
||||||
|
mediaSourceFactory: MediaSource.Factory,
|
||||||
|
mediaDataSourceFactory: DataSource.Factory
|
||||||
|
): MediaSource.Factory? {
|
||||||
|
// Return your custom factory or null to use default
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 4. `shouldDisableCache`
|
||||||
|
|
||||||
|
Enables dynamic disabling of the caching system per source.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
override fun shouldDisableCache(source: Source): Boolean {
|
||||||
|
return true // your own logic
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Once implemented, `react-native-video` will automatically invoke these methods for each `<Video />` instance.
|
||||||
|
|
||||||
|
## iOS Implementation
|
||||||
|
|
||||||
|
### 1. Podspec Integration
|
||||||
|
|
||||||
|
Your new module must have access to `react-native-video`. Add it as a dependency in your Podspec file:
|
||||||
|
|
||||||
|
```podfile
|
||||||
|
s.dependency "react-native-video"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create the Plugin
|
||||||
|
|
||||||
|
You can implement either the base `RNVPlugin` class or the player-specific `RNVAVPlayerPlugin` class.
|
||||||
|
|
||||||
|
#### Base Plugin
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class MyAnalyticsPlugin: RNVPlugin {
|
||||||
|
override func onInstanceCreated(id: String, player: Any) {
|
||||||
|
// Handle player creation
|
||||||
|
}
|
||||||
|
|
||||||
|
override func onInstanceRemoved(id: String, player: Any) {
|
||||||
|
// Handle player removal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### AVPlayer-Specific Plugin
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class MyAVPlayerAnalyticsPlugin: RNVAVPlayerPlugin {
|
||||||
|
override func onInstanceCreated(id: String, player: AVPlayer) {
|
||||||
|
// Handle AVPlayer creation with type-safe access
|
||||||
|
}
|
||||||
|
|
||||||
|
override func onInstanceRemoved(id: String, player: AVPlayer) {
|
||||||
|
// Handle AVPlayer removal with type-safe access
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optionally override the asset used by the player before playback starts
|
||||||
|
override func overridePlayerAsset(source: VideoSource, asset: AVAsset) async -> OverridePlayerAssetResult? {
|
||||||
|
// Return a modified asset or nil to use the default
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `RNVAVPlayerPlugin` class defines several extension points:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
/**
|
||||||
|
* Function called when a new AVPlayer instance is created
|
||||||
|
* @param id: a random string identifying the player
|
||||||
|
* @param player: the instantiated AVPlayer
|
||||||
|
*/
|
||||||
|
open func onInstanceCreated(id: String, player: AVPlayer) { /* no-op */ }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when an AVPlayer instance is being removed
|
||||||
|
* @param id: a random string identifying the player
|
||||||
|
* @param player: the AVPlayer to release
|
||||||
|
*/
|
||||||
|
open func onInstanceRemoved(id: String, player: AVPlayer) { /* no-op */ }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optionally override the asset used by the player before playback starts.
|
||||||
|
* Allows you to modify or replace the AVAsset before it is used to create the AVPlayerItem.
|
||||||
|
* Return nil to use the default asset.
|
||||||
|
*
|
||||||
|
* @param source: The VideoSource describing the video (uri, type, headers, etc.)
|
||||||
|
* @param asset: The AVAsset prepared by the player
|
||||||
|
* @return: OverridePlayerAssetResult if you want to override, or nil to use the default
|
||||||
|
*/
|
||||||
|
open func overridePlayerAsset(source: VideoSource, asset: AVAsset) async -> OverridePlayerAssetResult? { nil }
|
||||||
|
```
|
||||||
|
|
||||||
|
##### `OverridePlayerAssetResult` and `OverridePlayerAssetType`
|
||||||
|
|
||||||
|
To override the asset, return an `OverridePlayerAssetResult`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
public struct OverridePlayerAssetResult {
|
||||||
|
public let type: OverridePlayerAssetType
|
||||||
|
public let asset: AVAsset
|
||||||
|
|
||||||
|
public init(type: OverridePlayerAssetType, asset: AVAsset) {
|
||||||
|
self.type = type
|
||||||
|
self.asset = asset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum OverridePlayerAssetType {
|
||||||
|
case partial // Return a partially modified asset; will go through the default prepare process
|
||||||
|
case full // Return a fully modified asset; will skip the default prepare process
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Use `.partial` if you want the asset to continue through the player's normal preparation (e.g., for text tracks or metadata injection).
|
||||||
|
- Use `.full` if you want to provide a fully prepared asset that will be used as-is for playback.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```swift
|
||||||
|
override func overridePlayerAsset(source: VideoSource, asset: AVAsset) async -> OverridePlayerAssetResult? {
|
||||||
|
// Example: Replace the asset URL
|
||||||
|
let newAsset = AVAsset(url: URL(string: "https://example.com/override.mp4")!)
|
||||||
|
return Result(type: .full, asset: newAsset)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Only one plugin can override the player asset at a time. If multiple plugins implement this, only the first will be used.
|
||||||
|
|
||||||
|
### 3. Register the Plugin
|
||||||
|
|
||||||
|
To register the plugin in `react-native-video`, call:
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
ReactNativeVideoManager.shared.registerPlugin(plugin: plugin)
|
ReactNativeVideoManager.shared.registerPlugin(plugin: plugin)
|
||||||
```
|
```
|
||||||
|
|
||||||
The proposed integration register the instanciated class in file `VideoPluginSample` in the init function:
|
In the sample implementation, the plugin is registered inside the `VideoPluginSample` file within the `init` function:
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
import react_native_video
|
import react_native_video
|
||||||
@@ -122,4 +319,118 @@ override init() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Your native module can now track Player updates directly from Player reference and report to backend.
|
Once registered, your module can track player updates and report analytics data to your backend.
|
||||||
|
|
||||||
|
## Custom DRM Manager
|
||||||
|
|
||||||
|
You can provide a custom DRM manager through your plugin to handle DRM in a custom way. This is useful when you need to integrate with a specific DRM provider or implement custom DRM logic.
|
||||||
|
|
||||||
|
### Android Implementation
|
||||||
|
|
||||||
|
#### 1/ Create custom DRM manager
|
||||||
|
|
||||||
|
Create a class that implements the `DRMManagerSpec` interface:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class CustomDRMManager : DRMManagerSpec {
|
||||||
|
@Throws(UnsupportedDrmException::class)
|
||||||
|
override fun buildDrmSessionManager(uuid: UUID, drmProps: DRMProps): DrmSessionManager? {
|
||||||
|
// Your custom implementation for building DRM session manager
|
||||||
|
// Return null if the DRM scheme is not supported
|
||||||
|
// Throw UnsupportedDrmException if the DRM scheme is invalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2/ Register DRM manager in your plugin
|
||||||
|
|
||||||
|
Implement `getDRMManager()` in your ExoPlayer plugin to provide the custom DRM manager:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class CustomVideoPlugin : RNVExoplayerPlugin {
|
||||||
|
private val drmManager = CustomDRMManager()
|
||||||
|
|
||||||
|
override fun getDRMManager(): DRMManagerSpec? {
|
||||||
|
return drmManager
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInstanceCreated(id: String, player: ExoPlayer) {
|
||||||
|
// Handle player creation
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInstanceRemoved(id: String, player: ExoPlayer) {
|
||||||
|
// Handle player removal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### iOS Implementation
|
||||||
|
|
||||||
|
#### 1/ Create custom DRM manager
|
||||||
|
|
||||||
|
Create a class that implements the `DRMManagerSpec` protocol:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class CustomDRMManager: NSObject, DRMManagerSpec {
|
||||||
|
func createContentKeyRequest(
|
||||||
|
asset: AVContentKeyRecipient,
|
||||||
|
drmProps: DRMParams?,
|
||||||
|
reactTag: NSNumber?,
|
||||||
|
onVideoError: RCTDirectEventBlock?,
|
||||||
|
onGetLicense: RCTDirectEventBlock?
|
||||||
|
) {
|
||||||
|
// Initialize content key session and handle key request
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleContentKeyRequest(keyRequest: AVContentKeyRequest) {
|
||||||
|
// Process the content key request
|
||||||
|
}
|
||||||
|
|
||||||
|
func finishProcessingContentKeyRequest(keyRequest: AVContentKeyRequest, license: Data) throws {
|
||||||
|
// Finish processing the key request with the obtained license
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(_ error: Error, for keyRequest: AVContentKeyRequest) {
|
||||||
|
// Handle any errors during the DRM process
|
||||||
|
}
|
||||||
|
|
||||||
|
func setJSLicenseResult(license: String, licenseUrl: String) {
|
||||||
|
// Handle successful license acquisition from JS side
|
||||||
|
}
|
||||||
|
|
||||||
|
func setJSLicenseError(error: String, licenseUrl: String) {
|
||||||
|
// Handle license acquisition errors from JS side
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2/ Register DRM manager in your plugin
|
||||||
|
|
||||||
|
Implement `getDRMManager()` in your AVPlayer plugin to provide the custom DRM manager:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class CustomVideoPlugin: RNVAVPlayerPlugin {
|
||||||
|
override func getDRMManager() -> DRMManagerSpec? {
|
||||||
|
return CustomDRMManager()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func onInstanceCreated(id: String, player: AVPlayer) {
|
||||||
|
// Handle player creation
|
||||||
|
}
|
||||||
|
|
||||||
|
override func onInstanceRemoved(id: String, player: AVPlayer) {
|
||||||
|
// Handle player removal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Important notes about DRM managers:
|
||||||
|
|
||||||
|
1. Only one plugin can provide a DRM manager at a time. If multiple plugins try to provide DRM managers, only the first one will be used.
|
||||||
|
2. The custom DRM manager will be used for all video instances in the app.
|
||||||
|
3. If no custom DRM manager is provided:
|
||||||
|
- On iOS, the default FairPlay-based implementation will be used
|
||||||
|
- On Android, the default ExoPlayer DRM implementation will be used
|
||||||
|
4. The DRM manager must handle all DRM-related functionality:
|
||||||
|
- On iOS: key requests, license acquisition, and error handling through AVContentKeySession
|
||||||
|
- On Android: DRM session management and license acquisition through ExoPlayer's DrmSessionManager
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
## UI over react-native-video
|
This page lists open-source projects that can be helpful for your player implementation. <br>
|
||||||
- [react-native-video-controls](https://github.com/itsnubix/react-native-video-controls): First reference player UI
|
If you have a project that could benefit other users, feel free to open a PR to add it here.
|
||||||
- [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
|
|
||||||
|
|
||||||
## Other tools
|
## Our (TheWidlarzGroup) Libraries
|
||||||
- [react-native-track-player](https://github.com/doublesymmetry/react-native-track-player): A toolbox to control player over media session
|
- [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.
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
# Updating
|
# Updating
|
||||||
|
|
||||||
### Version 6.0.0
|
## Version 6.0.0
|
||||||
|
|
||||||
#### iOS
|
### iOS
|
||||||
|
|
||||||
##### Min iOS version
|
#### Minimum 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.
|
Starting from version 6.0.0, the minimum supported iOS version is 13.0. Projects using `react-native < 0.73` must set the minimum iOS version to 13.0 in the Podfile.
|
||||||
|
|
||||||
|
You can do this by adding the following code to your Podfile:
|
||||||
|
|
||||||
You can do it by adding the following code to your Podfile:
|
|
||||||
```diff
|
```diff
|
||||||
- platform :ios, min_ios_version_supported
|
- platform :ios, min_ios_version_supported
|
||||||
|
|
||||||
@@ -17,16 +18,16 @@ You can do it by adding the following code to your Podfile:
|
|||||||
+ end
|
+ end
|
||||||
```
|
```
|
||||||
|
|
||||||
##### linking
|
#### 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.
|
In your project's Podfile, add support for static dependency linking. This is required to support the new Promises subdependency in the iOS Swift conversion.
|
||||||
|
|
||||||
Add `use_frameworks! :linkage => :static` just under `platform :ios` in your ios project Podfile.
|
Add `use_frameworks! :linkage => :static` right below `platform :ios` in your iOS project Podfile.
|
||||||
|
|
||||||
[See the example ios project for reference](https://github.com/TheWidlarzGroup/react-native-video/blob/master/examples/basic/ios/Podfile#L5)
|
[See the example iOS project for reference](https://github.com/TheWidlarzGroup/react-native-video/blob/master/examples/basic/ios/Podfile#L5).
|
||||||
|
|
||||||
##### podspec
|
#### Podspec
|
||||||
|
|
||||||
You can remove following lines from your podfile as they are not necessary anymore
|
You can remove the following lines from your Podfile as they are no longer needed:
|
||||||
|
|
||||||
```diff
|
```diff
|
||||||
- `pod 'react-native-video', :path => '../node_modules/react-native-video/react-native-video.podspec'`
|
- `pod 'react-native-video', :path => '../node_modules/react-native-video/react-native-video.podspec'`
|
||||||
@@ -34,19 +35,35 @@ You can remove following lines from your podfile as they are not necessary anymo
|
|||||||
- `pod 'react-native-video/VideoCaching', :path => '../node_modules/react-native-video/react-native-video.podspec'`
|
- `pod 'react-native-video/VideoCaching', :path => '../node_modules/react-native-video/react-native-video.podspec'`
|
||||||
```
|
```
|
||||||
|
|
||||||
If you were previously using VideoCaching, you should $RNVideoUseVideoCaching flag in your podspec, see: [installation section](https://thewidlarzgroup.github.io/react-native-video/installation#video-caching)
|
If you were previously using VideoCaching, you should set the `$RNVideoUseVideoCaching` flag in your Podspec. See the [installation section](https://docs.thewidlarzgroup.com/react-native-video/installation#video-caching) for details.
|
||||||
|
|
||||||
#### Android
|
> **Note:** If you are enabling video caching (using `$RNVideoUseVideoCaching`), you must add the following to your `Gemfile`:
|
||||||
|
>
|
||||||
|
> ```ruby
|
||||||
|
> gem "cocoapods-swift-modular-headers"
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> Then, install dependencies using:
|
||||||
|
>
|
||||||
|
> ```sh
|
||||||
|
> bundle install
|
||||||
|
> bundle exec pod install
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> This enables Swift modular headers for Swift dependencies.
|
||||||
|
|
||||||
If you are already using Exoplayer on V5, you should remove the patch done from **android/settings.gradle**
|
### Android
|
||||||
|
|
||||||
|
If you were using ExoPlayer on V5, remove the patch from **android/settings.gradle**:
|
||||||
|
|
||||||
```diff
|
```diff
|
||||||
- include ':react-native-video'
|
- include ':react-native-video'
|
||||||
- project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer')
|
- 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
|
```groovy
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
@@ -66,4 +83,5 @@ allprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
If you encounter an error `Could not find com.android.support:support-annotations:27.0.0.` reinstall your Android Support Repository.
|
|
||||||
|
If you encounter the error `Could not find com.android.support:support-annotations:27.0.0.`, reinstall your Android Support Repository.
|
||||||
|
|||||||
BIN
docs/public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 569 B |
BIN
docs/public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
docs/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -1,4 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import TWGBadge from './components/TWGBadge/TWGBadge';
|
||||||
|
import jsonLd from './json-ld.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
head: (
|
head: (
|
||||||
@@ -13,7 +15,7 @@ export default {
|
|||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="og:image"
|
name="og:image"
|
||||||
content="https://thewidlarzgroup.github.io/react-native-video/thumbnail.jpg"
|
content="https://docs.thewidlarzgroup.com/react-native-video/thumbnail.jpg"
|
||||||
/>
|
/>
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:title" content="React Native Video" />
|
<meta name="twitter:title" content="React Native Video" />
|
||||||
@@ -23,14 +25,29 @@ export default {
|
|||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="twitter:image"
|
name="twitter:image"
|
||||||
content="https://thewidlarzgroup.github.io/react-native-video/thumbnail.jpg"
|
content="https://docs.thewidlarzgroup.com/react-native-video/thumbnail.jpg"
|
||||||
/>
|
/>
|
||||||
<meta name="twitter:image:alt" content="React Native Video" />
|
<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
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400..900&display=swap"
|
rel="icon"
|
||||||
rel="stylesheet"
|
type="image/x-icon"
|
||||||
|
href="https://docs.thewidlarzgroup.com/react-native-video/favicon.ico"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="32x32"
|
||||||
|
href="https://docs.thewidlarzgroup.com/react-native-video/favicon-32x32.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="16x16"
|
||||||
|
href="https://docs.thewidlarzgroup.com/react-native-video/favicon-16x16.png"
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -39,74 +56,28 @@ export default {
|
|||||||
🎬 <strong>Video component</strong> for React Native
|
🎬 <strong>Video component</strong> for React Native
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
faviconGlyph: '🎬',
|
|
||||||
project: {
|
project: {
|
||||||
link: 'https://github.com/TheWidlarzGroup/react-native-video',
|
link: 'https://github.com/TheWidlarzGroup/react-native-video',
|
||||||
},
|
},
|
||||||
docsRepositoryBase:
|
docsRepositoryBase:
|
||||||
'https://github.com/TheWidlarzGroup/react-native-video/tree/master/docs/',
|
'https://github.com/TheWidlarzGroup/react-native-video/tree/master/docs/',
|
||||||
|
main: ({children}) => (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<TWGBadge visibleOnLarge={false} />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
toc: {
|
||||||
|
extraContent: <TWGBadge visibleOnLarge={true} />,
|
||||||
|
},
|
||||||
footer: {
|
footer: {
|
||||||
text: (
|
text: (
|
||||||
<span>
|
<span>
|
||||||
Built with ❤️ by <strong>React Native Community</strong>
|
Built with ❤️ by <strong>TheWidlarzGroup</strong> &{' '}
|
||||||
|
<strong>React Native Community</strong>
|
||||||
</span>
|
</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() {
|
useNextSeoProps() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
|
|
||||||
[android]
|
|
||||||
target = Google Inc.:Google APIs:23
|
|
||||||
|
|
||||||
[maven_repositories]
|
|
||||||
central = https://repo1.maven.org/maven2
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
BUNDLE_PATH: "vendor/bundle"
|
|
||||||
BUNDLE_FORCE_RUBY_PLATFORM: 1
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
extends: '@react-native',
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: ['@typescript-eslint'],
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ['*.ts', '*.tsx'],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-shadow': ['error'],
|
|
||||||
'no-shadow': 'off',
|
|
||||||
'no-undef': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
67
examples/FabricExample/.gitignore
vendored
@@ -1,67 +0,0 @@
|
|||||||
# OSX
|
|
||||||
#
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Xcode
|
|
||||||
#
|
|
||||||
build/
|
|
||||||
*.pbxuser
|
|
||||||
!default.pbxuser
|
|
||||||
*.mode1v3
|
|
||||||
!default.mode1v3
|
|
||||||
*.mode2v3
|
|
||||||
!default.mode2v3
|
|
||||||
*.perspectivev3
|
|
||||||
!default.perspectivev3
|
|
||||||
xcuserdata
|
|
||||||
*.xccheckout
|
|
||||||
*.moved-aside
|
|
||||||
DerivedData
|
|
||||||
*.hmap
|
|
||||||
*.ipa
|
|
||||||
*.xcuserstate
|
|
||||||
ios/.xcode.env.local
|
|
||||||
|
|
||||||
# Android/IntelliJ
|
|
||||||
#
|
|
||||||
build/
|
|
||||||
.idea
|
|
||||||
.gradle
|
|
||||||
local.properties
|
|
||||||
*.iml
|
|
||||||
*.hprof
|
|
||||||
.cxx/
|
|
||||||
|
|
||||||
# node.js
|
|
||||||
#
|
|
||||||
node_modules/
|
|
||||||
npm-debug.log
|
|
||||||
yarn-error.log
|
|
||||||
|
|
||||||
# BUCK
|
|
||||||
buck-out/
|
|
||||||
\.buckd/
|
|
||||||
*.keystore
|
|
||||||
!debug.keystore
|
|
||||||
|
|
||||||
# fastlane
|
|
||||||
#
|
|
||||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
|
||||||
# screenshots whenever they are needed.
|
|
||||||
# For more information about the recommended setup visit:
|
|
||||||
# https://docs.fastlane.tools/best-practices/source-control/
|
|
||||||
|
|
||||||
**/fastlane/report.xml
|
|
||||||
**/fastlane/Preview.html
|
|
||||||
**/fastlane/screenshots
|
|
||||||
**/fastlane/test_output
|
|
||||||
|
|
||||||
# Bundle artifact
|
|
||||||
*.jsbundle
|
|
||||||
|
|
||||||
# Ruby / CocoaPods
|
|
||||||
/ios/Pods/
|
|
||||||
/vendor/bundle/
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
arrowParens: 'avoid',
|
|
||||||
bracketSameLine: true,
|
|
||||||
bracketSpacing: false,
|
|
||||||
singleQuote: true,
|
|
||||||
trailingComma: 'all',
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
2.7.5
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
/**
|
|
||||||
* Sample React Native App
|
|
||||||
* https://github.com/facebook/react-native
|
|
||||||
*
|
|
||||||
* Generated with the TypeScript template
|
|
||||||
* https://github.com/react-native-community/react-native-template-typescript
|
|
||||||
*
|
|
||||||
* @format
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, {type PropsWithChildren} from 'react';
|
|
||||||
import {StyleSheet, View} from 'react-native';
|
|
||||||
import VideoPlayer from './src/VideoPlayer';
|
|
||||||
|
|
||||||
const App = () => {
|
|
||||||
return <VideoPlayer />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
source 'https://rubygems.org'
|
|
||||||
|
|
||||||
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
|
||||||
ruby ">= 2.6.10"
|
|
||||||
|
|
||||||
gem 'cocoapods', '~> 1.13'
|
|
||||||
gem 'activesupport', '>= 6.1.7.3', '< 7.1.0'
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* @format
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'react-native';
|
|
||||||
import React from 'react';
|
|
||||||
import App from '../App';
|
|
||||||
|
|
||||||
// Note: import explicitly to use the types shipped with jest.
|
|
||||||
import {it} from '@jest/globals';
|
|
||||||
|
|
||||||
// Note: test renderer must be required after react-native.
|
|
||||||
import renderer from 'react-test-renderer';
|
|
||||||
|
|
||||||
it('renders correctly', () => {
|
|
||||||
renderer.create(<App />);
|
|
||||||
});
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# To learn about Buck see [Docs](https://buckbuild.com/).
|
|
||||||
# To run your application with Buck:
|
|
||||||
# - install Buck
|
|
||||||
# - `npm start` - to start the packager
|
|
||||||
# - `cd android`
|
|
||||||
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
|
|
||||||
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
|
|
||||||
# - `buck install -r android/app` - compile, install and run application
|
|
||||||
#
|
|
||||||
|
|
||||||
load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
|
|
||||||
|
|
||||||
lib_deps = []
|
|
||||||
|
|
||||||
create_aar_targets(glob(["libs/*.aar"]))
|
|
||||||
|
|
||||||
create_jar_targets(glob(["libs/*.jar"]))
|
|
||||||
|
|
||||||
android_library(
|
|
||||||
name = "all-libs",
|
|
||||||
exported_deps = lib_deps,
|
|
||||||
)
|
|
||||||
|
|
||||||
android_library(
|
|
||||||
name = "app-code",
|
|
||||||
srcs = glob([
|
|
||||||
"src/main/java/**/*.java",
|
|
||||||
]),
|
|
||||||
deps = [
|
|
||||||
":all-libs",
|
|
||||||
":build_config",
|
|
||||||
":res",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
android_build_config(
|
|
||||||
name = "build_config",
|
|
||||||
package = "net.video.fabricexample",
|
|
||||||
)
|
|
||||||
|
|
||||||
android_resource(
|
|
||||||
name = "res",
|
|
||||||
package = "net.video.fabricexample",
|
|
||||||
res = "src/main/res",
|
|
||||||
)
|
|
||||||
|
|
||||||
android_binary(
|
|
||||||
name = "app",
|
|
||||||
keystore = "//android/keystores:debug",
|
|
||||||
manifest = "src/main/AndroidManifest.xml",
|
|
||||||
package_type = "debug",
|
|
||||||
deps = [
|
|
||||||
":app-code",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
apply plugin: "com.android.application"
|
|
||||||
apply plugin: "org.jetbrains.kotlin.android"
|
|
||||||
apply plugin: "com.facebook.react"
|
|
||||||
|
|
||||||
import com.android.build.OutputFile
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the configuration block to customize your React Native Android app.
|
|
||||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
|
||||||
*/
|
|
||||||
react {
|
|
||||||
/* Folders */
|
|
||||||
// The root of your project, i.e. where "package.json" lives. Default is '..'
|
|
||||||
// root = file("../")
|
|
||||||
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
|
|
||||||
// reactNativeDir = file("../node-modules/react-native")
|
|
||||||
// The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
|
|
||||||
// codegenDir = file("../node_modules/@react-native/codegen")
|
|
||||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
|
|
||||||
// cliFile = file("../node_modules/react-native/cli.js")
|
|
||||||
|
|
||||||
/* Variants */
|
|
||||||
// The list of variants to that are debuggable. For those we're going to
|
|
||||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
|
||||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
|
||||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
|
||||||
|
|
||||||
/* Bundling */
|
|
||||||
// A list containing the node command and its flags. Default is just 'node'.
|
|
||||||
// nodeExecutableAndArgs = ["node"]
|
|
||||||
//
|
|
||||||
// The command to run when bundling. By default is 'bundle'
|
|
||||||
// bundleCommand = "ram-bundle"
|
|
||||||
//
|
|
||||||
// The path to the CLI configuration file. Default is empty.
|
|
||||||
// bundleConfig = file(../rn-cli.config.js)
|
|
||||||
//
|
|
||||||
// The name of the generated asset file containing your JS bundle
|
|
||||||
// bundleAssetName = "MyApplication.android.bundle"
|
|
||||||
//
|
|
||||||
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
|
||||||
// entryFile = file("../js/MyApplication.android.js")
|
|
||||||
//
|
|
||||||
// A list of extra flags to pass to the 'bundle' commands.
|
|
||||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
|
||||||
// extraPackagerArgs = []
|
|
||||||
|
|
||||||
/* Hermes Commands */
|
|
||||||
// The hermes compiler command to run. By default it is 'hermesc'
|
|
||||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
|
||||||
//
|
|
||||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
|
||||||
// hermesFlags = ["-O", "-output-source-map"]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
|
||||||
*/
|
|
||||||
def enableProguardInReleaseBuilds = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The preferred build flavor of JavaScriptCore (JSC)
|
|
||||||
*
|
|
||||||
* For example, to use the international variant, you can use:
|
|
||||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
|
||||||
*
|
|
||||||
* The international variant includes ICU i18n library and necessary data
|
|
||||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
|
||||||
* give correct results when using with locales other than en-US. Note that
|
|
||||||
* this variant is about 6MiB larger per architecture than default.
|
|
||||||
*/
|
|
||||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
|
||||||
|
|
||||||
android {
|
|
||||||
ndkVersion rootProject.ext.ndkVersion
|
|
||||||
|
|
||||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
|
||||||
compileSdk rootProject.ext.compileSdkVersion
|
|
||||||
|
|
||||||
namespace "net.video.fabricexample"
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "net.video.fabricexample"
|
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
|
||||||
versionCode 1
|
|
||||||
versionName "1.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
signingConfigs {
|
|
||||||
debug {
|
|
||||||
storeFile file('debug.keystore')
|
|
||||||
storePassword 'android'
|
|
||||||
keyAlias 'androiddebugkey'
|
|
||||||
keyPassword 'android'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildTypes {
|
|
||||||
debug {
|
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
}
|
|
||||||
release {
|
|
||||||
// Caution! In production, you need to generate your own keystore file.
|
|
||||||
// see https://reactnative.dev/docs/signed-apk-android.
|
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
minifyEnabled enableProguardInReleaseBuilds
|
|
||||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
// The version of react-native is set by the React Native Gradle Plugin
|
|
||||||
implementation("com.facebook.react:react-android")
|
|
||||||
implementation("com.facebook.react:flipper-integration")
|
|
||||||
|
|
||||||
if (hermesEnabled.toBoolean()) {
|
|
||||||
implementation("com.facebook.react:hermes-android")
|
|
||||||
} else {
|
|
||||||
implementation jscFlavor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
"""Helper definitions to glob .aar and .jar targets"""
|
|
||||||
|
|
||||||
def create_aar_targets(aarfiles):
|
|
||||||
for aarfile in aarfiles:
|
|
||||||
name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")]
|
|
||||||
lib_deps.append(":" + name)
|
|
||||||
android_prebuilt_aar(
|
|
||||||
name = name,
|
|
||||||
aar = aarfile,
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_jar_targets(jarfiles):
|
|
||||||
for jarfile in jarfiles:
|
|
||||||
name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")]
|
|
||||||
lib_deps.append(":" + name)
|
|
||||||
prebuilt_jar(
|
|
||||||
name = name,
|
|
||||||
binary_jar = jarfile,
|
|
||||||
)
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# By default, the flags in this file are appended to flags specified
|
|
||||||
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
|
||||||
# You can edit the include path and order by changing the proguardFiles
|
|
||||||
# directive in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="net.video.fabricexample">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:name=".MainApplication"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:allowBackup="false"
|
|
||||||
android:theme="@style/AppTheme">
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:windowSoftInputMode="adjustResize"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package net.video.fabricexample
|
|
||||||
|
|
||||||
import com.facebook.react.ReactActivity
|
|
||||||
import com.facebook.react.ReactActivityDelegate
|
|
||||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
|
||||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
|
||||||
|
|
||||||
class MainActivity : ReactActivity() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
|
||||||
* rendering of the component.
|
|
||||||
*/
|
|
||||||
override fun getMainComponentName(): String = "FabricExample"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
|
||||||
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
|
||||||
*/
|
|
||||||
override fun createReactActivityDelegate(): ReactActivityDelegate =
|
|
||||||
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package net.video.fabricexample
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import com.facebook.react.PackageList
|
|
||||||
import com.facebook.react.ReactApplication
|
|
||||||
import com.facebook.react.ReactHost
|
|
||||||
import com.facebook.react.ReactNativeHost
|
|
||||||
import com.facebook.react.ReactPackage
|
|
||||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
|
||||||
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
|
||||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
|
||||||
import com.facebook.react.flipper.ReactNativeFlipper
|
|
||||||
import com.facebook.soloader.SoLoader
|
|
||||||
|
|
||||||
class MainApplication : Application(), ReactApplication {
|
|
||||||
|
|
||||||
override val reactNativeHost: ReactNativeHost =
|
|
||||||
object : DefaultReactNativeHost(this) {
|
|
||||||
override fun getPackages(): List<ReactPackage> =
|
|
||||||
PackageList(this).packages.apply {
|
|
||||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
|
||||||
// add(MyReactNativePackage())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getJSMainModuleName(): String = "index"
|
|
||||||
|
|
||||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
|
||||||
|
|
||||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
|
||||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
|
||||||
}
|
|
||||||
|
|
||||||
override val reactHost: ReactHost
|
|
||||||
get() = getDefaultReactHost(this.applicationContext, reactNativeHost)
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
SoLoader.init(this, false)
|
|
||||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
|
||||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager)
|
|
||||||
}
|
|
||||||
}
|
|
||||||