chore: publish package

This commit is contained in:
Krzysztof Moch
2025-06-30 19:32:12 +02:00
parent 18527fcc5c
commit 4ba3b7a61f
525 changed files with 26973 additions and 35284 deletions

15
.editorconfig Normal file
View File

@@ -0,0 +1,15 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

View File

@@ -1,2 +0,0 @@
examples/
lib/

View File

@@ -1,16 +0,0 @@
{
"plugins": ["@typescript-eslint"],
"extends": [
"@react-native",
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"no-trailing-spaces": 1
},
"parserOptions": {
"requireConfigFile": false
}
}

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
*.pbxproj -text
# specific for windows script files
*.bat text eol=crlf

3
.github/FUNDING.yml vendored
View File

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

View File

@@ -1,18 +0,0 @@
### Current behavior
Describe what happens when you encounter this issue.
### Reproduction steps
A 1, 2, 3, etc. list of what's needed to see the issue happen.
### Expected behavior
Describe what you wanted to happen
### Platform
Which player are you experiencing the problem on:
* iOS
* Android
* Windows UWP
* Windows WPF
### Video sample
If possible, include a link to the video that has the problem that can be streamed or downloaded from.

View File

@@ -1,92 +0,0 @@
name: Bug report
description: Create a report to help us improve
title: "[BUG]: "
labels: ["bug"]
assignees: []
body:
- type: markdown
attributes:
value: Thanks for taking the time to fill out this bug report! Please do not report issue on 5.2.1 version, this version is not maintained anymore. Only issues on version > V6 will be handled. Please also ensure your issue is reproduced with the last release!
- type: input
id: version
attributes:
label: Version
description: What version are you using? Put the exact version from your package.json
validations:
required: true
- type: dropdown
id: platforms
validations:
required: true
attributes:
label: What platforms are you having the problem on?
multiple: true
options:
- iOS
- Android
- Windows
- visionOS
- Android TV
- Apple tvOS
- type: input
id: system_version
attributes:
label: System Version
description: What version of the system is using device that you are experiencing the issue?
validations:
required: true
- type: dropdown
id: device
validations:
required: true
attributes:
label: On what device are you experiencing the issue?
multiple: true
options:
- Real device
- Simulator
- type: dropdown
id: architecture
attributes:
label: Architecture
description: What architecture are you using?
options:
- Old architecture
- New architecture with interop layer
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: input
id: reproduction-repo
attributes:
label: Reproduction Link
description: Provide a link to a repository with a reproduction of the bug, this is optional but it will make us to fix the bug faster
placeholder: Reproduction Repository
value: "repository link"
validations:
required: false
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Tell us how can we reproduce this bug
placeholder: Reproduction
value: "Step to reproduce this bug are: "
validations:
required: true

View File

@@ -1,11 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: GitHub Discussions
url: https://github.com/TheWidlarzGroup/react-native-video/discussions
about: Please ask and answer questions here.
- name: Slack Channel
url: https://join.slack.com/t/video-dev/shared_invite/zt-24kgmctuv-2z0O9J_v6q_rg~x1RujdOA
about: You can find us on the VideoDev Slack in the react-native-video channel.
- name: TheWidlarzGroup Discord
url: https://discord.gg/7Y6eE62hXM
about: Feel free to join our Discord server and ask questions there.

View File

@@ -1,59 +0,0 @@
name: Feature request
description: Suggest an idea for this project
title: "[Feature]: "
labels: ["feature"]
assignees: []
body:
- type: markdown
attributes:
value: Thanks for taking the time to fill out this feature report!
- type: textarea
id: description
attributes:
label: Description
description: Tell us your idea and why will concern if we implement it. You can also create a PR 😄
placeholder: Tell us your idea!
value: "Very cool idea!"
validations:
required: true
- type: textarea
id: why-it-is-needed
attributes:
label: Why it is needed ?
description: Tell us your why it is needed!
placeholder: Why it is needed ?
value: "Because it is cool!"
validations:
required: true
- type: textarea
id: possible-implementation
attributes:
label: Possible implementation
description: |
Tell us your possible implementation! It really helps if you could describe from a technical POV how this new feature would work, which code it rely on, etc
placeholder: How to implement ?
value: "Technical POV how to do it"
validations:
required: false
- type: textarea
id: code-sample
attributes:
label: Code sample
description: Please show how the new code could work, if doable
placeholder: Code sample
value: "Code sample"
validations:
required: false
- type: markdown
attributes:
value: |
## Support
If this functionality is important to you and you need it, contact [TheWidlarzGroup](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=feature-request&utm_campaign=issue-template&utm_id=sponsorship#Contact) - [`hi@thewidlarzgroup.com`](mailto:hi@thewidlarzgroup.com)

View File

@@ -1,25 +0,0 @@
<!--
Thanks for opening a PR!
Since this is a volunteer project and is very active, anything you can do to reduce the amount of time needed to review and merge your PR is appreciated.
The following steps will help get your PR merged quickly:
- Update the documentation
If you've added new functionality, update the README.md with an entry for your prop or event.
The entry should be inserted in alphabetical order.
- Provide an example of how to test the change
If the PR requires special testing setup provide all the relevant instructions and files. This may include a sample video file or URL, configuration, or setup steps.
- Focus the PR on only one area
If you're touching multiple different areas that aren't related, break the changes up into multiple PRs.
- Describe the changes
Add a note describing what your PR does. If there is a change to the behavior of the code, explain why it needs to be updated.
-->
## Summary
### Motivation
### Changes
## Test plan

View File

@@ -1,34 +0,0 @@
name: setup bun
description: Setup bun and install dependencies
inputs:
working-directory:
description: 'working directory for bun install'
default: ./
required: false
runs:
using: composite
steps:
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.0.4
- name: Cache dependencies
id: bun-cache
uses: actions/cache@v4
with:
path: |
**/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
${{ runner.os }}-bun-
- name: Install dependencies
working-directory: ${{ inputs.working-directory }}
if: steps.bun-cache.outputs.cache-hit != 'true'
run: bun install
shell: bash

View File

@@ -1,34 +0,0 @@
name: Setup node_modules
description: Setup Node.js and install dependencies
inputs:
working-directory:
description: 'working directory for yarn install'
default: ./
required: false
runs:
using: composite
steps:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Cache dependencies
id: yarn-cache
uses: actions/cache@v4
with:
path: |
${{ inputs.working-directory }}/node_modules
.yarn/install-state.gz
key: ${{ runner.os }}-yarn-${{ inputs.working-directory }}-${{ hashFiles('yarn.lock') }}-${{ hashFiles('**/package.json', '!node_modules/**') }}
restore-keys: |
${{ runner.os }}-yarn-${{ inputs.working-directory }}-${{ hashFiles('yarn.lock') }}-
${{ runner.os }}-yarn-${{ inputs.working-directory }}
- name: Install dependencies
working-directory: ${{ inputs.working-directory }}
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --immutable --ignore-scripts
shell: bash

27
.github/actions/setup/action.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Setup
description: Setup Node.js and install dependencies
runs:
using: composite
steps:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version-file: .nvmrc
- name: Cache dependencies
id: yarn-cache
uses: actions/cache@v3
with:
path: |
**/node_modules
.yarn/install-state.gz
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-${{ hashFiles('**/package.json', '!node_modules/**') }}
restore-keys: |
${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
${{ runner.os }}-yarn-
- name: Install dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --immutable
shell: bash

View File

@@ -1,323 +0,0 @@
const FIELD_MAPPINGS = {
Platform: 'What platforms are you having the problem on?',
Version: 'Version',
SystemVersion: 'System Version',
DeviceType: 'On what device are you experiencing the issue?',
Architecture: 'Architecture',
Description: 'What happened?',
ReproductionLink: 'Reproduction Link',
Reproduction: 'Reproduction',
};
const PLATFORM_LABELS = {
iOS: 'Platform: iOS',
visionOS: 'Platform: iOS',
'Apple tvOS': 'Platform: iOS',
Android: 'Platform: Android',
'Android TV': 'Platform: Android',
Windows: 'Platform: Windows',
web: 'Platform: Web',
};
const BOT_LABELS = [
'Missing Info',
'Repro Provided',
'Missing Repro',
'Waiting for Review',
'Newer Version Available',
'6.x.x',
'7.0',
...Object.values(PLATFORM_LABELS),
];
const SKIP_LABEL = 'No Validation';
const ISSUE_BOOST_INFO = (issueNumber) => `
Need faster resolution? Consider [Issue Boost](https://www.thewidlarzgroup.com/issue-boost/?utm_source=rnv&utm_medium=bug-report&utm_campaign=bot-message&utm_id=${issueNumber}) it allows us to dedicate time specifically to your issue and fix it faster 🚀`;
const MESSAGE = {
FEATURE_REQUEST: (issueNumber) => `Thanks for the feature request! 🚀
\nYou can check out our [public roadmap](https://github.com/orgs/TheWidlarzGroup/projects/6) to see what we're currently working on. All requests are automatically added there, so you can track progress anytime.
\nWe review and implement new features when time allows, but this can take a while. If you'd like to speed things up and make this a priority, consider [Issue Boost](https://www.thewidlarzgroup.com/issue-boost/?utm_source=rnv&utm_medium=feature-request&utm_campaign=bot-message&utm_id=${issueNumber}), our commercial option that lets us dedicate time specifically to your request.
\nThanks for your input and patience! 🙌`,
BUG_REPORT: (issueNumber) => `Hey! 👋
Thanks for reporting this issue. We try to fix bugs as quickly as possible, but since our time is limited, we prioritize sponsored issues first, then focus on critical problems affecting many users, and finally, we handle other reports when we can. Some issues might take a while to be resolved.
\nIf you want to speed up this process, check out [Issue Boost](https://www.thewidlarzgroup.com/issue-boost/?utm_source=rnv&utm_medium=bug-report&utm_campaign=bot-message-valid&utm_id=${issueNumber}) it allows us to dedicate time specifically to your issue and fix it faster.
\nThanks for your patience and support! 🚀`,
MISSING_INFO: (missingFields) => {
return `Hey! 👋
Thanks for the bug report. To help us resolve your issue effectively, we still need some key information:\n\n${missingFields
.map((field) => `- ${field.replace('missing-', '')}`)
.join('\n')}
Please edit your issue and fill in the missing details.
> Issues with incomplete info are treated with lower priority, so this helps speed things up.`;
},
OUTDATED_VERSION: (issueVersion, latestVersion) => {
return (
`Heads up! ⚠️ You're using version **${issueVersion}**, but the latest stable version is **${latestVersion}**. ` +
`Please update to the newest version and check if the issue still occurs.\n\n` +
`> Keeping your dependencies up-to-date often resolves many common problems.` +
`\n\nStill having the issue after upgrading? Update the report with the new version details so we can investigate.`
);
},
};
const checkLatestVersion = async () => {
try {
const response = await fetch(
'https://registry.npmjs.org/react-native-video/latest',
);
const data = await response.json();
return data.version;
} catch (error) {
console.error('Error checking latest version:', error);
return null;
}
};
const getFieldValue = (body, field) => {
if (!FIELD_MAPPINGS[field]) {
console.warn('Field not supported:', field);
return '';
}
const fieldValue = FIELD_MAPPINGS[field];
const sections = body.split('###');
const section = sections.find((section) => {
// Find the section that contains the field
// For Reproduction, we need to make sure that we don't match Reproduction Link
if (field === 'Reproduction') {
return (
section.includes(fieldValue) && !section.includes('Reproduction Link')
);
}
return section.includes(fieldValue);
});
return section ? section.replace(fieldValue, '').trim() : '';
};
const validateBugReport = async (body, labels) => {
const selectedPlatforms = getFieldValue(body, 'Platform')
.split(',')
.map((p) => p.trim());
if (selectedPlatforms.length === 0) {
labels.add('missing-platform');
} else {
selectedPlatforms.forEach((platform) => {
const label = PLATFORM_LABELS[platform];
if (label) {
labels.add(label);
} else {
console.warn('Platform not supported', platform);
}
});
}
const version = getFieldValue(body, 'Version');
if (version) {
const words = version.split(' ');
const versionPattern = /\d+\.\d+\.\d+/;
const isVersionValid = words.some((word) => versionPattern.test(word));
if (!isVersionValid) {
labels.add('missing-version');
} else {
// Add version-specific labels
const versionMatch = words.find((word) => versionPattern.test(word));
if (versionMatch) {
const majorVersion = versionMatch.split('.')[0];
if (majorVersion === '6') {
labels.add('6.x.x');
} else if (majorVersion === '7') {
labels.add('7.0');
}
}
}
const latestVersion = await checkLatestVersion();
if (latestVersion && latestVersion !== version) {
labels.add(`outdated-version-${version}-${latestVersion}`);
}
}
const fields = [
{
name: 'SystemVersion',
invalidValue:
'What version of the system is using device that you are experiencing the issue?',
},
{name: 'DeviceType'},
{name: 'Architecture'},
{name: 'Description', invalidValue: 'A bug happened!'},
{name: 'Reproduction', invalidValue: 'Step to reproduce this bug are:'},
{name: 'ReproductionLink', invalidValue: 'repository link'},
];
fields.forEach(({name, invalidValue}) => {
const value = getFieldValue(body, name);
if (!value || value === invalidValue) {
const fieldName = FIELD_MAPPINGS[name];
labels.add(`missing-${fieldName.toLowerCase()}`);
}
});
};
const validateFeatureRequest = (body, labels) => {
// Implement feature request validation logic here
};
const handleIssue = async ({github, context}) => {
const {issue} = context.payload;
const {body} = issue;
const labels = new Set(issue.labels.map((label) => label.name));
if (labels.has(SKIP_LABEL)) {
console.log('Skiping Issue Validation');
return;
}
// Clear out labels that are added by the bot
BOT_LABELS.forEach((label) => labels.delete(label));
const isBug = labels.has('bug');
const isFeature = labels.has('feature');
if (isFeature) {
await handleFeatureRequest({github, context, body, labels});
} else if (isBug) {
await handleBugReport({github, context, body, labels});
} else {
console.warn('Issue is not a bug or feature request');
}
await updateIssueLabels({github, context, labels});
};
const handleFeatureRequest = async ({github, context, body, labels}) => {
validateFeatureRequest(body, labels);
const comment = MESSAGE.FEATURE_REQUEST(context.payload.issue.number);
await hidePreviousComments({github, context});
await createComment({github, context, body: comment});
};
const handleBugReport = async ({github, context, body, labels}) => {
await validateBugReport(body, labels);
if (Array.from(labels).some((label) => label.startsWith('missing-'))) {
await handleMissingInformation({github, context, labels});
} else {
await handleValidReport({github, context, labels});
}
};
const handleMissingInformation = async ({github, context, labels}) => {
const missingFields = Array.from(labels).filter((label) =>
label.startsWith('missing-'),
);
const outdatedVersionLabel = Array.from(labels).find((label) =>
label.startsWith('outdated-version'),
);
if (missingFields.length > 0) {
let comment = MESSAGE.MISSING_INFO(missingFields);
if (outdatedVersionLabel) {
const [, , issueVersion, latestVersion] = outdatedVersionLabel.split('-');
comment += `\n\n ${MESSAGE.OUTDATED_VERSION(
issueVersion,
latestVersion,
)}`;
}
comment += `\n\n${ISSUE_BOOST_INFO(context.payload.issue.number)}`;
await hidePreviousComments({github, context});
await createComment({github, context, body: comment});
}
updateLabelsForMissingInfo(labels);
};
const handleValidReport = async ({github, context, labels}) => {
let comment = MESSAGE.BUG_REPORT(context.payload.issue.number);
const outdatedVersionLabel = Array.from(labels).find((label) =>
label.startsWith('outdated-version'),
);
if (outdatedVersionLabel) {
const [, , issueVersion, latestVersion] = outdatedVersionLabel.split('-');
comment += `\n\n ${MESSAGE.OUTDATED_VERSION(issueVersion, latestVersion)}`;
labels.add('Newer Version Available');
}
await hidePreviousComments({github, context});
await createComment({github, context, body: comment});
labels.add('Repro Provided');
labels.add('Waiting for Review');
};
const createComment = async ({github, context, body}) => {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
body,
});
};
const updateIssueLabels = async ({github, context, labels}) => {
const labelsToAdd = Array.from(labels).filter(
(label) => !label.startsWith('missing-') && !label.startsWith('outdated-'),
);
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
labels: labelsToAdd,
});
};
const hidePreviousComments = async ({github, context}) => {
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
});
// Filter for bot comments that aren't already hidden
const unhiddenBotComments = comments.data.filter(
(comment) =>
comment.user.type === 'Bot' &&
!comment.body.includes('<details>') &&
!comment.body.includes('Previous bot comment'),
);
for (const comment of unhiddenBotComments) {
// Don't format string - it will broke the markdown
const hiddenBody = `
<details>
<summary>Previous bot comment (click to expand)</summary>
${comment.body}
</details>`;
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: comment.id,
body: hiddenBody,
});
}
};
module.exports = handleIssue;

View File

@@ -1,96 +0,0 @@
name: Build Android
on:
push:
branches:
- master
paths:
- '.github/workflows/build-android.yml'
- 'android/**'
- 'examples/bare/android/**'
- 'yarn.lock'
- 'examples/bare/yarn.lock'
pull_request:
paths:
- '.github/workflows/build-android.yml'
- 'android/**'
- 'examples/bare/android/**'
- 'yarn.lock'
- 'examples/bare/yarn.lock'
jobs:
build:
name: Build Android Example App
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup JDK 17
uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: 17
java-package: jdk
- name: Install node_modules at Root
uses: ./.github/actions/setup-node
with:
working-directory: ./
- name: Build Library
run: yarn build
- name: Install node_modules at Example
uses: ./.github/actions/setup-node
with:
working-directory: examples/bare
- name: Restore Gradle cache
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Run Gradle Build for bare example
run: cd examples/bare/android && ./gradlew assembleDebug --build-cache && cd ../../..
build-with-ads:
name: Build Android Example App With Ads
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup JDK 17
uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: 17
java-package: jdk
- name: Install node_modules at Root
uses: ./.github/actions/setup-node
with:
working-directory: ./
- name: Build Library
run: yarn build
- name: Install node_modules at Example
uses: ./.github/actions/setup-node
with:
working-directory: examples/bare
- name: Restore Gradle cache
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Run Gradle Build for bare example
run: cd examples/bare/android && export RNV_SAMPLE_ENABLE_ADS=true && ./gradlew assembleDebug --build-cache && cd ../../..

View File

@@ -1,214 +0,0 @@
name: Build iOS
on:
workflow_dispatch:
push:
branches:
- master
paths:
- '.github/workflows/build-ios.yml'
- 'ios/**'
- '*.podspec'
- 'examples/bare/ios/**'
pull_request:
paths:
- '.github/workflows/build-ios.yml'
- 'ios/**'
- '*.podspec'
- 'examples/bare/ios/**'
jobs:
build:
name: Build iOS Example App
runs-on: macos-latest
defaults:
run:
working-directory: examples/bare/ios
steps:
- uses: actions/checkout@v4
- name: Install node_modules at Root
uses: ./.github/actions/setup-node
with:
working-directory: ./
- name: Build Library
working-directory: ./
run: yarn build
- name: Install node_modules at Example
uses: ./.github/actions/setup-node
with:
working-directory: examples/bare
- name: Restore buildcache
uses: mikehardy/buildcache-action@v2
continue-on-error: true
- name: Setup Ruby (bundle)
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6.10
bundler-cache: true
- name: Restore Pods cache
uses: actions/cache@v4
with:
path: |
examples/bare/ios/Pods
~/Library/Caches/CocoaPods
~/.cocoapods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-
- name: Generate Native Project
run: pod install
- name: Install Pods
run: pod install
- name: Install xcpretty
run: gem install xcpretty
- name: Build App
run: "set -o pipefail && xcodebuild \
-derivedDataPath build -UseModernBuildSystem=YES \
-workspace BareExample.xcworkspace \
-scheme BareExample \
-sdk iphonesimulator \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 14' \
build \
CODE_SIGNING_ALLOWED=NO | xcpretty"
build-with-ads:
name: Build iOS Example App With Ads
runs-on: macos-latest
defaults:
run:
working-directory: examples/bare/ios
steps:
- uses: actions/checkout@v4
- name: Install node_modules at Root
uses: ./.github/actions/setup-node
with:
working-directory: ./
- name: Build Library
working-directory: ./
run: yarn build
- name: Install node_modules at Example
uses: ./.github/actions/setup-node
with:
working-directory: examples/bare
- name: Restore buildcache
uses: mikehardy/buildcache-action@v2
continue-on-error: true
- name: Setup Ruby (bundle)
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6.10
bundler-cache: true
- name: Restore Pods cache
uses: actions/cache@v4
with:
path: |
examples/bare/ios/Pods
~/Library/Caches/CocoaPods
~/.cocoapods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-
- name: Generate Native Project
run: export RNV_SAMPLE_ENABLE_ADS=true && pod install
- name: Install Pods
run: export RNV_SAMPLE_ENABLE_ADS=true && pod install
- name: Install xcpretty
run: gem install xcpretty
- name: Build App
run: "set -o pipefail && export RNV_SAMPLE_ENABLE_ADS=true && xcodebuild \
-derivedDataPath build -UseModernBuildSystem=YES \
-workspace BareExample.xcworkspace \
-scheme BareExample \
-sdk iphonesimulator \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 14' \
build \
CODE_SIGNING_ALLOWED=NO | xcpretty"
build-with-caching:
name: Build iOS Example App With Caching
runs-on: macos-latest
defaults:
run:
working-directory: examples/bare/ios
steps:
- uses: actions/checkout@v4
- name: Install node_modules at Root
uses: ./.github/actions/setup-node
with:
working-directory: ./
- name: Build Library
working-directory: ./
run: yarn build
- name: Install node_modules at Example
uses: ./.github/actions/setup-node
with:
working-directory: examples/bare
- name: Restore buildcache
uses: mikehardy/buildcache-action@v2
continue-on-error: true
- name: Setup Ruby (bundle)
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6.10
bundler-cache: true
- name: Restore Pods cache
uses: actions/cache@v4
with:
path: |
examples/bare/ios/Pods
~/Library/Caches/CocoaPods
~/.cocoapods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-
- name: Install gem dependencies
run: bundle install
- name: Generate Native Project
run: export RNV_SAMPLE_VIDEO_CACHING=true && bundle exec pod install
- name: Install Pods
run: export RNV_SAMPLE_VIDEO_CACHING=true && bundle exec pod install
- name: Install xcpretty
run: gem install xcpretty
- name: Build App
run: "set -o pipefail && export RNV_SAMPLE_VIDEO_CACHING=true && xcodebuild \
-derivedDataPath build -UseModernBuildSystem=YES \
-workspace BareExample.xcworkspace \
-scheme BareExample \
-sdk iphonesimulator \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 14' \
build \
CODE_SIGNING_ALLOWED=NO | xcpretty"

View File

@@ -1,34 +0,0 @@
name: Check Android
on:
push:
branches:
- master
paths:
- '.github/workflows/check-android.yml'
- 'android/**'
pull_request:
paths:
- '.github/workflows/check-android.yml'
- 'android/**'
jobs:
Kotlin-Lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.1/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/
- name: run ktlint
working-directory: ./android/
run: |
ktlint --reporter=checkstyle,output=build/ktlint-report.xml --relative --editorconfig=./.editorconfig
continue-on-error: true
- uses: yutailang0119/action-ktlint@v3
with:
report-path: ./android/build/*.xml
continue-on-error: false
- uses: actions/upload-artifact@v4
with:
name: ktlint-report
path: ./android/build/*.xml

View File

@@ -1,31 +0,0 @@
name: Check CLang
on:
push:
branches:
- master
paths:
- '.github/workflows/check-clang.yml'
- 'ios/**'
pull_request:
branches:
- master
paths:
- '.github/workflows/check-clang.yml'
- 'ios/**'
jobs:
CLang-Format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install clang-format
run: sudo apt-get install clang-format
- name: Check ios clang formatting
run: |
find ios -type f \( -name "*.h" -o -name "*.cpp" -o -name "*.m" -o -name "*.mm" \) -print0 | while read -d $'\0' file; do
clang-format -style=file:./ios/.clang-format -i "$file"
done
shell: bash
- name: Check for changes
run: git diff --exit-code HEAD

View File

@@ -1,41 +0,0 @@
name: Check iOS
on:
push:
branches:
- master
paths:
- '.github/workflows/check-ios.yml'
- 'ios/**'
pull_request:
paths:
- '.github/workflows/check-ios.yml'
- 'ios/**'
jobs:
Swift-Lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint with SwiftLint
uses: norio-nomura/action-swiftlint@master
with:
args: --strict
env:
WORKING_DIRECTORY: ios
Swift-Format:
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
defaults:
run:
working-directory: ./ios
steps:
- uses: actions/checkout@v4
- name: Install SwiftFormat
run: brew install swiftformat
- name: Format Swift code
run: swiftformat --verbose .
- name: Verify formatted code is unchanged
run: git diff --exit-code HEAD

View File

@@ -1,62 +0,0 @@
name: Check JS
on:
push:
branches:
- master
paths:
- '.github/workflows/check-js.yml'
- 'src/**'
- '*.json'
- '*.js'
- '*.jsx'
- '*.ts'
- '*.tsx'
- '*.lock'
pull_request:
paths:
- '.github/workflows/check-js.yml'
- 'src/**'
- '*.json'
- '*.js'
- '*.jsx'
- '*.ts'
- '*.tsx'
- '*.lock'
jobs:
TypeScript-Check:
name: Check TS (tsc)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install node_modules
uses: ./.github/actions/setup-node
- name: Install reviewdog
uses: reviewdog/action-setup@v1
- name: Check TypeScript
run: |
yarn tsc | reviewdog -name="tsc" -efm="%f(%l,%c): error TS%n: %m" -reporter="github-pr-review" -filter-mode="nofilter" -fail-on-error -tee
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JS-Lint:
name: Lint JS (eslint, prettier)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install node_modules
uses: ./.github/actions/setup-node
- name: Run ESLint
run: yarn lint -f @jamesacarr/github-actions
- name: Run ESLint with auto-fix
run: yarn lint --fix
- name: Verify no files have changed after auto-fix
run: git diff --exit-code HEAD

157
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,157 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
merge_group:
types:
- checks_requested
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
- name: Lint files
run: yarn lint
- name: Typecheck files
run: yarn typecheck
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
- name: Run unit tests
run: yarn test --maxWorkers=2 --coverage
build-library:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
- name: Build package
run: yarn prepare
build-android:
runs-on: ubuntu-latest
env:
TURBO_CACHE_DIR: .turbo/android
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
- name: Cache turborepo for Android
uses: actions/cache@v3
with:
path: ${{ env.TURBO_CACHE_DIR }}
key: ${{ runner.os }}-turborepo-android-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-turborepo-android-
- name: Check turborepo cache for Android
run: |
TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:android').cache.status")
if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then
echo "turbo_cache_hit=1" >> $GITHUB_ENV
fi
- name: Install JDK
if: env.turbo_cache_hit != 1
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
- name: Finalize Android SDK
if: env.turbo_cache_hit != 1
run: |
/bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null"
- name: Cache Gradle
if: env.turbo_cache_hit != 1
uses: actions/cache@v3
with:
path: |
~/.gradle/wrapper
~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('example/android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build example for Android
env:
JAVA_OPTS: "-XX:MaxHeapSize=6g"
run: |
yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}"
build-ios:
runs-on: macos-14
env:
TURBO_CACHE_DIR: .turbo/ios
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
- name: Cache turborepo for iOS
uses: actions/cache@v3
with:
path: ${{ env.TURBO_CACHE_DIR }}
key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-turborepo-ios-
- name: Check turborepo cache for iOS
run: |
TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status")
if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then
echo "turbo_cache_hit=1" >> $GITHUB_ENV
fi
- name: Cache cocoapods
if: env.turbo_cache_hit != 1
id: cocoapods-cache
uses: actions/cache@v3
with:
path: |
**/ios/Pods
key: ${{ runner.os }}-cocoapods-${{ hashFiles('example/ios/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-cocoapods-
- name: Install cocoapods
if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true'
run: |
cd example/ios
pod install
env:
NO_FLIPPER: 1
- name: Build example for iOS
run: |
yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}"

View File

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

View File

@@ -1,24 +0,0 @@
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
workflow_dispatch:
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
days-before-issue-stale: 30
days-before-issue-close: 14
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity. If there won't be any activity in the next 14 days, this issue will be closed automatically."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
exempt-issue-labels: "feature,Accepted,good first issue"
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

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

View File

@@ -1,19 +0,0 @@
name: Issue Validator and Labeler
on:
issues:
types: [opened, edited]
jobs:
validate-and-label:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate Issue Template and Add Labels
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const script = require('./.github/scripts/validate.js')
await script({github, context})

60
.gitignore vendored
View File

@@ -2,6 +2,13 @@
#
.DS_Store
# XDE
.expo/
# VSCode
.vscode/
jsconfig.json
# Xcode
#
build/
@@ -21,29 +28,31 @@ DerivedData
*.ipa
*.xcuserstate
project.xcworkspace
Pods
# Android/IJ
#
*.iml
.idea
.classpath
.cxx
.gradle
local.properties
*.hprof
.idea
.project
.settings
.classpath
local.properties
android.iml
# Cocoapods
#
example/ios/Pods
# Ruby
example/vendor/
# node.js
#
node_modules/
*.log
# yarn
yarn.lock
# editor workspace settings
.vscode
npm-debug.log
yarn-debug.log
yarn-error.log
# BUCK
buck-out/
@@ -51,12 +60,23 @@ buck-out/
android/app/libs
android/keystores/debug.keystore
# windows
Deploy.binlog
msbuild.binlog
android/buildOutput_*
# Yarn
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# lib build
# Expo
.expo/
# Turborepo
.turbo/
# generated by bob
lib/
!src/lib
*.tsbuildinfo
# React Native Codegen
ios/generated
android/generated

View File

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

2
.nvmrc
View File

@@ -1 +1 @@
v18
v18

View File

@@ -1,6 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": false,
"jsxBracketSameLine": true
}

View File

@@ -1,27 +0,0 @@
{
"git": {
"commitMessage": "chore: release v${version}",
"requireCleanWorkingDir": true,
"tagAnnotation": "Release v${version}",
"tagName": "v${version}"
},
"plugins": {
"@release-it/conventional-changelog": {
"preset": "angular",
"infile": "CHANGELOG.md"
}
},
"hooks": {
"before:init": [
"rm -Rf lib tsconfig.tsbuildinfo",
"yarn install --frozen-lockfile --non-interactive --production=false",
"yarn run lint",
"yarn run build"
],
"after:release": "echo Successfully released ${name} v${version} from repository ${repo.repository}."
},
"npm": {
"skipChecks": false
}
}

1
.watchmanconfig Normal file
View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

874
.yarn/releases/yarn-3.6.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

10
.yarnrc.yml Normal file
View File

@@ -0,0 +1,10 @@
nodeLinker: node-modules
nmHoistingLimits: workspaces
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
yarnPath: .yarn/releases/yarn-3.6.1.cjs

File diff suppressed because it is too large Load Diff

133
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,133 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[INSERT CONTACT METHOD].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -1,53 +1,122 @@
## Issues
# Contributing
* New issues are reviewed and if they require additional work will be marked with the [`triage needed`](https://github.com/TheWidlarzGroup/react-native-video/labels/triage%20needed) label. This is an open call for help from the community to verify the issue and help categorize it. If an issue stays in this state for a long time, it will be closed unresolved.
* Once an issue has been reviewed it will be labeled with [`help wanted`](https://github.com/TheWidlarzGroup/react-native-video/labels/help%20wanted) to indicate it is ready to be worked on. Please wait for this label before submitting a PR to avoid spending time on something that is likely to be rejected.
Contributions are always welcome, no matter how large or small!
## Cleanup
We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md).
* Given the history of this project, we are going to be more aggressive than usual in keeping things clean. We are working with limited resources and do not want to return to the 1000+ open issues state. This is not meant to be disrespectful or hostile. It is just a way to keep the limited resources we have focused. If your issue was closed prematurely, just chime in and engage!
* Issues and pull requests that become stale (60 days of inactivity) will be closed unless assigned and show progress.
* If the issue creator fails to provide additional information within a week when asked, we may close the issue to keep things tidy (but you can always comment back and we can reopen).
## Development workflow
## Pull Requests
This project is a monorepo managed using [Yarn workspaces](https://yarnpkg.com/features/workspaces). It contains the following packages:
* Please open an issue before opening a PR to make sure the proposed change is wanted and is likely to be merged. We don't want you to waste your time!
* Pull requests require 1-3 approved reviews to be merged.
* The number of reviews depends on the complexity by adding up (max of 3):
* `1` reviewer for each PR
* `1` if more than 3 files and/or 30 lines of code changed
* `1` for each native platform code changes involved
- The library package in the root directory.
- An example app in the `example/` directory.
For example, a single file JS code change requires 1 review while a 3 files iOS code change requires 3 reviews. As soon as the reviews show up as approved without any requested changes, the PR will be merged into the next milestone.
* Reviewers will be asked to assign a risk level when they are done from 1 (super safe) to 5 (super risky). A release with any risk level 4 or 5 will be published as a major version, otherwise as a patch or minor based on the changes. Prepare for some large version increments while we get more comfortable... (but remember versions are free).
* 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).
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:
To get started with the project, run `yarn` in the root directory to install the required dependencies for each package:
```sh
cd docs
bun install
bun dev
yarn
```
### Publishing a release
> Since the project relies on Yarn workspaces, you cannot use [`npm`](https://github.com/npm/cli) for development.
We use [release-it](https://github.com/webpro/release-it) to automate our release.
The [example app](/example/) demonstrates usage of the library. You need to run it to test any changes you make.
## Reporting issues
It is configured to use the local version of the library, so any changes you make to the library's source code will be reflected in the example app. Changes to the library's JavaScript code will be reflected in the example app without a rebuild, but native code changes will require a rebuild of the example app.
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.
If you want to use Android Studio or XCode to edit the native code, you can open the `example/android` or `example/ios` directories respectively in those editors. To edit the Objective-C or Swift files, open `example/ios/VideoExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > react-native-video`.
## License
To edit the Java or Kotlin files, open `example/android` in Android studio and find the source files at `react-native-video` under `Android`.
By contributing to React Native Video, you agree that your contributions will be licensed under its **MIT** license.
You can use various commands from the root directory to work with the project.
To start the packager:
```sh
yarn example start
```
To run the example app on Android:
```sh
yarn example android
```
To run the example app on iOS:
```sh
yarn example ios
```
Make sure your code passes TypeScript and ESLint. Run the following to verify:
```sh
yarn typecheck
yarn lint
```
To fix formatting errors, run the following:
```sh
yarn lint --fix
```
Remember to add tests for your change if possible. Run the unit tests by:
```sh
yarn test
```
### Commit message convention
We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages:
- `fix`: bug fixes, e.g. fix crash due to deprecated method.
- `feat`: new features, e.g. add new method to the module.
- `refactor`: code refactor, e.g. migrate from class components to hooks.
- `docs`: changes into documentation, e.g. add usage example for the module..
- `test`: adding or updating tests, e.g. add integration tests using detox.
- `chore`: tooling changes, e.g. change CI config.
Our pre-commit hooks verify that your commit message matches this format when committing.
### Linting and tests
[ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/)
We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing.
Our pre-commit hooks verify that the linter and tests pass when committing.
### Publishing to npm
We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc.
To publish new versions, run the following:
```sh
yarn release
```
### Scripts
The `package.json` file contains various scripts for common tasks:
- `yarn`: setup project by installing dependencies.
- `yarn typecheck`: type-check files with TypeScript.
- `yarn lint`: lint files with ESLint.
- `yarn test`: run unit tests with Jest.
- `yarn example start`: start the Metro server for the example app.
- `yarn example android`: run the example app on Android.
- `yarn example ios`: run the example app on iOS.
### Sending a pull request
> **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github).
When you're sending a pull request:
- Prefer small pull requests focused on one change.
- Verify that linters and tests are passing.
- Review the documentation to make sure it looks good.
- Follow the pull request template when opening a pull request.
- For pull requests that change the API or implementation, discuss with maintainers first by opening an issue.

48
LICENSE
View File

@@ -1,22 +1,34 @@
MIT License
Custom License
Copyright (c) 2016-2022 Project contributors
Copyright (c) 2016 Brent Vatne, Baris Sencan
License Terms
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Copyright (c) 2024 TheWidlarzGroup
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
Scope of Use
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This code is licensed for demonstration and contribution purposes only. You may view, execute, and contribute to this code solely within the designated repository (“Repository”) or an authorized fork of the Repository on GitHub for the sole purpose of contributing changes back to the Repository.
Forking and Contributions
You are permitted to create a fork of the Repository on GitHub exclusively for the purpose of contributing changes back to the original Repository.
Any fork of the Repository is subject to the same terms as this license and may not be used, distributed, or modified beyond the scope of preparing contributions to the original Repository.
All contributions to the Repository or any authorized fork are automatically licensed to the original author under the same terms as this license.
Prohibition on External Modification and Distribution
Modifying, adapting, or creating derivative works based on this code outside of the Repository or an authorized fork is strictly prohibited. Distribution of this code, in whole or in part, outside of GitHub without explicit written permission from the original author is also forbidden.
Non-Commercial Use Only, with Author Permission for Commercial Use
This code may not be used for commercial purposes. Any use of the code in a product, service, or other commercial setting is strictly prohibited unless explicit permission is granted in writing by TheWidlarzGroup.
TheWidlarzGroup reserves the right to grant commercial usage privileges at its discretion.
No Warranty
This code is provided "as is," without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. In no event shall the author be liable for any claim, damages, or other liability arising from or in connection with the code or the use or other dealings in the code.
Termination
Any violation of these terms will result in the immediate termination of this license.
Acceptance
By using, forking, and contributing to this code, you acknowledge that you have read and understood these terms and agree to be bound by them.

View File

@@ -3,8 +3,10 @@ require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
fabric_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
Pod::Spec.new do |s|
s.name = "react-native-video-plugin-sample"
s.name = "NitroVideo"
s.version = package["version"]
s.summary = package["description"]
s.homepage = package["homepage"]
@@ -12,10 +14,22 @@ Pod::Spec.new do |s|
s.authors = package["author"]
s.platforms = { :ios => min_ios_version_supported }
s.source = { :git => ".git", :tag => "#{s.version}" }
s.source = { :git => "https://github.com/KrzysztofMoch/react-native-video.git", :tag => "#{s.version}" }
s.source_files = "ios/**/*.{h,m,mm,swift}"
s.dependency "react-native-video"
s.source_files = [
"ios/*.{h,m,mm,swift}",
"ios/hybrids/*.{h,m,mm,swift}", # Nitro Hybrid files
"ios/view/**/*.{h,m,mm,swift}" # Video View files
]
if fabric_enabled
s.exclude_files = ["ios/view/paper/**/*.{h,m,mm,swift}"]
else
s.exclude_files = ["ios/view/fabric/**/*.{h,m,mm,swift}"]
end
# Cxx to Swift bridging helpers
s.public_header_files = ["ios/Video-Bridging-Header.h"]
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
@@ -25,13 +39,14 @@ Pod::Spec.new do |s|
s.dependency "React-Core"
# Don't install the dependencies when we run `pod install` in the old architecture.
if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
if fabric_enabled then
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
"OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
}
s.dependency "React-RCTFabric"
s.dependency "React-Codegen"
s.dependency "RCT-Folly"
s.dependency "RCTRequired"
@@ -39,4 +54,8 @@ Pod::Spec.new do |s|
s.dependency "ReactCommon/turbomodule/core"
end
end
# Add all files generated by Nitrogen
load 'nitrogen/generated/ios/NitroVideo+autolinking.rb'
add_nitrogen_files(s)
end

125
README.md
View File

@@ -1,122 +1,35 @@
[![React Native Video Component](./docs/assets/baners/rnv-banner.png)](https://thewidlarzgroup.com/?utm_source=rnv&utm_medium=readme&utm_id=banner)
# react-native-video
The most battle-tested open-source video player component for React Native with support for DRM, offline playback, HLS/DASH streaming, and more.
<Video /> Component for React Native
## Installation
## 🔍 Features
- 📱 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
## ✨ Project Status
| Version | State | Architecture |
|---------|-------|--------------|
| **v5 and lower** | ❌ End-of-life [Commercial Support Available](https://www.thewidlarzgroup.com/blog/react-native-video-upgrade-challenges-custom-maintenance-support#how-we-can-help?utm_source=rnv&utm_medium=readme&utm_id=upgradev5) | Old Architecture |
| **v6** | 🛠 Maintained (community + TWG) | Old + New (Interop Layer) |
| **v7** | 🚀 Active Development | Old + New (Full Support) |
`react-native-video` v7 introduces full support for the new React Native architecture, unlocking better performance, improved consistency, and modern native modules.
---
## 📚 Documentation & Examples
- 📖 [Documentation](https://docs.thewidlarzgroup.com/react-native-video/)
- 📦 [Example: Free DRM Stream](https://www.thewidlarzgroup.com/services/free-drm-token-generator-for-video?utm_source=rnv&utm_medium=readme&utm_id=free-drm)
- 📦 [Example: Offline SDK integration](https://docs.thewidlarzgroup.com/offline-video-sdk)
## 🚀 Quick Start
### Install
```bash
# Install dependencies
yarn add react-native-video
# Install pods
cd ios && pod install
```sh
npm install react-native-video
```
### Usage
```tsx
import Video from 'react-native-video';
## Usage
export default () => (
<Video
source={{ uri: 'https://www.w3schools.com/html/mov_bbb.mp4' }}
style={{ width: '100%', aspectRatio: 16 / 9 }}
controls
/>
);
```js
import { VideoView } from "react-native-video";
// ...
<VideoView color="tomato" />
```
---
## 🧩 Plugins
## Contributing
<a href="https://www.thewidlarzgroup.com/offline-video-sdk?utm_source=rnv&utm_medium=readme&utm_id=banner">
<img src="./docs/assets/baners/sdk-banner.png" alt="Offline SDK Preview" width="40%" align="right" />
</a>
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
### 1 · 📥 Offline SDK
## License
Enable offline streaming with full control over downloads, license lifecycle, secure storage, and media access.
[Custom](LICENSE)
- Track selection (bitrate, audio, subtitles)
- Pause / resume & background queueing
- Expiration & auto-cleanup
- Built for Android & iOS
- → [Read the SDK Docs](https://docs.thewidlarzgroup.com/offline-video-sdk?utm_source=rnv&utm_medium=readme&utm_id=modules-sdk-text)
### 2 · 🧪 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/other/plugin)
This project is provided solely for demonstration and contribution purposes. Forking is permitted exclusively for submitting changes to the [main repository](https://github.com/TheWidlarzGroup/react-native-video-v7). The code and its modifications may only be used within this repository or an authorized fork. Commercial use of the code is prohibited unless you have permission from TheWidlarzGroup
---
## 💼 TWG Services & Products
| Offering | Description |
|----------|-------------|
| [**Professional Support Packages**](https://www.thewidlarzgroup.com/issue-boost?utm_source=rnv&utm_medium=readme&utm_campaign=professional-support-packages#Contact) | Priority bug-fixes, guaranteed SLAs, [roadmap influence](https://github.com/orgs/TheWidlarzGroup/projects/6) |
| [**Issue Booster**](https://www.thewidlarzgroup.com/issue-boost?utm_source=rnv&utm_medium=readme) | Fast-track urgent fixes with a payperissue model |
| [**Offline Video SDK**](https://www.thewidlarzgroup.com/offline-video-sdk/?utm_source=rnv&utm_medium=readme&utm_campaign=downloading&utm_id=offline-video-sdk-link) | Plugandplay secure download solution for iOS & Android |
| [**Integration Support**](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=readme&utm_campaign=integration-support#Contact) | Handson help integrating video, DRM & offline into your app |
| [**Free DRM Token Generator**](https://www.thewidlarzgroup.com/services/free-drm-token-generator-for-video?utm_source=rnv&utm_medium=readme&utm_id=free-drm) | Generate Widevine / FairPlay tokens for testing |
| [**Ready Boilerplates**](https://www.thewidlarzgroup.com/showcases?utm_source=rnv&utm_medium=readme) | Ready-to-use apps with offline HLS/DASH DRM, video frame scrubbing, TikTok-style video feed, background uploads, Skia-based frame processor (R&D phase), and more |
| [**React Native Video Upgrade Guide**](https://www.thewidlarzgroup.com/blog/react-native-video-upgrade-challenges-custom-maintenance-support?utm_source=rnv&utm_medium=readme&utm_id=upgrade-blog&utm_campaign=v7) | Common upgrade pitfalls & how to solve them |
*See how [TWG](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=readme&utm_id=services-text) helped **Learnn** ship a worldclass player in record time - [case study](https://gitnation.com/contents/a-4-year-retrospective-lessons-learned-from-building-a-video-player-from-scratch-with-react-native).*
Contact us at [hi@thewidlarzgroup.com](mailto:hi@thewidlarzgroup.com)
## 🌍 Social
- 🐦 **X / Twitter** - [follow product & release updates](https://x.com/TheWidlarzGroup)
- 💬 **Discord** - [talk to the community and us](https://discord.gg/9WPq6Yx)
- 💼 **LinkedIn** - [see TWG flexing](https://linkedin.com/company/the-widlarz-group)
## 📰 Community & Media
- 🗽 **React Summit US** How TWG helped Learnn boost video performance on React Native.
[Watch the talk »](https://gitnation.com/contents/a-4-year-retrospective-lessons-learned-from-building-a-video-player-from-scratch-with-react-native)
- 🧨 **v7 deep dive** Why were building v7 with Nitro Modules
[Watch on X »](https://x.com/krzysztof_moch/status/1854162551946478051)
- 🛠️ **Well-maintained open-source library** - What does it truly mean? - Bart's talk for React Native Warsaw
[Watch here »](https://www.youtube.com/watch?v=RAQQwGCQNqY)
- 📺 **“Over the Top” Panel** - Building Streaming Apps for Mobile, Web, and Smart TVs - Bart giving his insights on the industry
[Watch here »](https://youtu.be/j2b_bG-32JI)
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)

5334
THIRD-PARTY-LICENSES Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +0,0 @@
[*.{kt,kts}]
indent_style=space
indent_size=4
continuation_indent_size=4
insert_final_newline=true
max_line_length=160
ktlint_code_style=android_studio
ktlint_standard=enabled
ktlint_experimental=enabled
ktlint_standard_filename=disabled # dont require PascalCase filenames
ktlint_standard_no-wildcard-imports=disabled # allow .* imports
ktlint_function_signature_body_expression_wrapping=multiline
ij_kotlin_allow_trailing_comma_on_call_site=false
ij_kotlin_allow_trailing_comma=false

29
android/CMakeLists.txt Normal file
View File

@@ -0,0 +1,29 @@
project(NitroVideo)
cmake_minimum_required(VERSION 3.9.0)
set (PACKAGE_NAME NitroVideo)
set (CMAKE_VERBOSE_MAKEFILE ON)
set (CMAKE_CXX_STANDARD 20)
# Define C++ library and add all sources
add_library(${PACKAGE_NAME} SHARED
src/main/cpp/cpp-adapter.cpp
)
# Add Nitrogen specs :)
include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/NitroVideo+autolinking.cmake)
# Set up local includes
include_directories(
"src/main/cpp"
)
find_library(LOG_LIB log)
# Link all libraries together
target_link_libraries(
${PACKAGE_NAME}
${LOG_LIB}
android # <-- Android core
)

View File

@@ -1,46 +0,0 @@
## react-native-video - ExoPlayer
This is an Android React Native video component based on ExoPlayer v2.
> ExoPlayer is an application level media player for Android. It provides an alternative to Androids MediaPlayer API for playing audio and video both locally and over the Internet. ExoPlayer supports features not currently supported by Androids MediaPlayer API, including DASH and SmoothStreaming adaptive playbacks. Unlike the MediaPlayer API, ExoPlayer is easy to customize and extend, and can be updated through Play Store application updates.
https://github.com/google/ExoPlayer
## Benefits over `react-native-video@0.9.0`:
- Android Video library built by Google, with a lot of support
- Supports DASH, HLS, & SmoothStreaming adaptive streams
- Supports formats such as MP4, M4A, FMP4, WebM, MKV, MP3, Ogg, WAV, MPEG-TS, MPEG-PS, FLV and ADTS (AAC).
- Fewer device specific issues
- Highly customisable
## ExoPlayer only props
```javascript
render() {
return (
<Video
...
disableFocus={true} // disables audio focus and wake lock (default false)
onAudioBecomingNoisy={this.onAudioBecomingNoisy} // Callback when audio is becoming noisy - should pause video
onAudioFocusChanged={this.onAudioFocusChanged} // Callback when audio focus has been lost - pause if focus has been lost
/>
)
}
onAudioBecomingNoisy = () => {
this.setState({ pause: true })
}
onAudioFocusChanged = (event: { hasAudioFocus: boolean }) => {
if (!this.state.paused && !event.hasAudioFocus) {
this.setState({ paused: true })
}
}
```
## Unimplemented props
- Expansion file - `source={{ mainVer: 1, patchVer: 0 }}`

View File

@@ -1,314 +1,174 @@
import com.android.Version
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
buildscript {
def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['RNVideo_kotlinVersion']
def requiredKotlinVersion = project.properties['RNVideo_kotlinVersion']
// Buildscript is evaluated before everything else so we can't use getExtOrDefault
def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["Video_kotlinVersion"]
def androidx_version = rootProject.ext.has('androidxActivityVersion') ? rootProject.ext.get('androidxActivityVersion') : project.properties['RNVideo_androidxActivityVersion']
def requiredAndroidxVersion = project.properties['RNVideo_androidxActivityVersion']
repositories {
google()
mavenCentral()
}
def isVersionAtLeast = { version, requiredVersion ->
def (v1, v2) = [version, requiredVersion].collect { it.tokenize('.')*.toInteger() }
for (int i = 0; i < Math.max(v1.size(), v2.size()); i++) {
int val1 = i < v1.size() ? v1[i] : 0
int val2 = i < v2.size() ? v2[i] : 0
if (val1 < val2) {
return false
} else if (val1 > val2) {
return true
}
}
return true
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
ext {
if (!isVersionAtLeast(kotlin_version, requiredKotlinVersion)) {
throw new GradleException("Kotlin version mismatch: Project is using Kotlin version $kotlin_version, but it must be at least $requiredKotlinVersion. Please update the Kotlin version.")
} else {
println("Kotlin version is correct: $kotlin_version")
}
if (!isVersionAtLeast(androidx_version, requiredAndroidxVersion)) {
throw new GradleException("AndroidX version mismatch: Project is using AndroidX version $androidx_version, but it must be at least $requiredAndroidxVersion. Please update the AndroidX version.")
} else {
println("AndroidX version is correct: $androidx_version")
}
}
}
// This looks funny but it's necessary to keep backwards compatibility (:
def safeExtGet(prop) {
return rootProject.ext.has(prop) ?
rootProject.ext.get(prop) : rootProject.ext.has("RNVideo_" + prop) ?
rootProject.ext.get("RNVideo_" + prop) : project.properties["RNVideo_" + prop]
}
def isNewArchitectureEnabled() {
return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
}
def supportsNamespace() {
def parsed = Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
def major = parsed[0].toInteger()
def minor = parsed[1].toInteger()
// Namespace support was added in 7.3.0
if (major == 7 && minor >= 3) {
return true
}
return major >= 8
}
def ExoplayerDependenciesList = [
"useExoplayerIMA",
"useExoplayerSmoothStreaming",
"useExoplayerDash",
"useExoplayerHls",
"useExoplayerRtsp",
]
def media3_buildFromSource = safeExtGet('buildFromMedia3Source').toBoolean() ?: false
def ExoplayerDependencies = ExoplayerDependenciesList.collectEntries { property ->
[(property): safeExtGet(property)?.toBoolean() ?: false]
}
ExoplayerDependenciesList.each { propertyName ->
def propertyValue = ExoplayerDependencies[propertyName]
println "$propertyName: $propertyValue"
}
println "buildFromSource: $media3_buildFromSource"
// This string is used to define build path.
// As react native build output directory is react-native path of the module.
// We need to force a new path on each configuration change.
// If you add a new build parameter, please add the new value in this string
def configStringPath = ExoplayerDependencies
.collect { property, value -> property + value}
.join('')
.concat("buildFromSource:$media3_buildFromSource")
.md5()
// commented as new architecture not yet fully supported
// if (isNewArchitectureEnabled()) {
// apply plugin: "com.facebook.react"
// }
android {
if (supportsNamespace()) {
namespace 'com.brentvatne.react'
sourceSets {
main {
manifest.srcFile "src/main/AndroidManifestNew.xml"
}
}
}
compileSdkVersion safeExtGet('compileSdkVersion')
buildToolsVersion safeExtGet('buildToolsVersion')
def agpVersion = Version.ANDROID_GRADLE_PLUGIN_VERSION
if (agpVersion.tokenize('.')[0].toInteger() < 8) {
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.majorVersion
}
}
defaultConfig {
minSdkVersion safeExtGet('minSdkVersion')
targetSdkVersion safeExtGet('targetSdkVersion')
versionCode 1
versionName "1.0"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
buildConfigField "boolean", "USE_EXOPLAYER_IMA", ExoplayerDependencies["useExoplayerIMA"].toString()
buildConfigField "boolean", "USE_EXOPLAYER_SMOOTH_STREAMING", ExoplayerDependencies["useExoplayerSmoothStreaming"].toString()
buildConfigField "boolean", "USE_EXOPLAYER_DASH", ExoplayerDependencies["useExoplayerDash"].toString()
buildConfigField "boolean", "USE_EXOPLAYER_HLS", ExoplayerDependencies["useExoplayerHls"].toString()
buildConfigField "boolean", "USE_EXOPLAYER_RTSP", ExoplayerDependencies["useExoplayerRtsp"].toString()
ndk {
abiFilters(*reactNativeArchitectures())
}
}
buildFeatures {
buildConfig true
}
packagingOptions {
exclude "**/libreact_render*.so"
}
buildDir 'buildOutput_' + configStringPath
sourceSets {
main {
java {
if (ExoplayerDependencies["useExoplayerIMA"]) {
exclude 'com/google/ads/interactivemedia/v3/api'
exclude 'androidx/media3/exoplayer/ima'
}
if (ExoplayerDependencies["useExoplayerSmoothStreaming"]) {
exclude 'androidx/media3/exoplayer/smoothstreaming'
}
if (ExoplayerDependencies["useExoplayerDash"]) {
exclude 'androidx/media3/exoplayer/dash'
}
if (ExoplayerDependencies["useExoplayerHls"]) {
exclude 'androidx/media3/exoplayer/hls'
}
if (ExoplayerDependencies["useExoplayerRtsp"]) {
exclude 'androidx/media3/exoplayer/rtsp'
}
}
}
}
sourceSets.main {
java {
if (isNewArchitectureEnabled()) {
srcDirs += [
"src/fabric/java",
"${project.buildDir}/generated/source/codegen/java"
]
} else {
srcDirs += [
"src/oldarch/java"
]
}
}
}
dependencies {
classpath "com.android.tools.build:gradle:7.2.1"
// noinspection DifferentKotlinGradleVersion
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
def value = rootProject.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}
def isNewArchitectureEnabled() {
return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
}
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
apply from: '../nitrogen/generated/android/NitroVideo+autolinking.gradle'
if (isNewArchitectureEnabled()) {
apply plugin: "com.facebook.react"
}
def getExtOrDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["Video_" + name]
}
def getExtOrIntegerDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["Video_" + name]).toInteger()
}
def supportsNamespace() {
def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
def major = parsed[0].toInteger()
def minor = parsed[1].toInteger()
// Namespace support was added in 7.3.0
return (major == 7 && minor >= 3) || major >= 8
}
android {
if (supportsNamespace()) {
namespace "com.video"
sourceSets {
main {
manifest.srcFile "src/main/AndroidManifestNew.xml"
}
}
}
ndkVersion getExtOrDefault("ndkVersion")
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
defaultConfig {
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
externalNativeBuild {
cmake {
cppFlags "-O2 -frtti -fexceptions -Wall -fstack-protector-all"
arguments "-DANDROID_STL=c++_shared"
abiFilters (*reactNativeArchitectures())
}
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
packagingOptions {
excludes = [
"META-INF",
"META-INF/**",
"**/libc++_shared.so",
"**/libfbjni.so",
"**/libjsi.so",
"**/libfolly_json.so",
"**/libfolly_runtime.so",
"**/libglog.so",
"**/libhermes.so",
"**/libhermes-executor-debug.so",
"**/libhermes_executor.so",
"**/libreactnativejni.so",
"**/libturbomodulejsijni.so",
"**/libreact_nativemodule_core.so",
"**/libjscexecutor.so"
]
}
buildFeatures {
buildConfig true
prefab true
}
buildTypes {
release {
minifyEnabled false
}
}
lintOptions {
disable "GradleCompatible"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
main {
if (!isNewArchitectureEnabled()) {
java.srcDirs += ["src/paper/java"]
}
}
}
}
repositories {
google()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android"
}
mavenCentral()
mavenCentral()
google()
}
def media3_version = safeExtGet('media3Version')
def androidxCore_version = safeExtGet('androidxCoreVersion')
def androidxActivity_version = safeExtGet('androidxActivityVersion')
def kotlin_version = getExtOrDefault("kotlinVersion")
dependencies {
// For < 0.71, this will be from the local maven repo
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+"
// For < 0.71, this will be from the local maven repo
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.core:core-ktx:$androidxCore_version"
implementation "androidx.activity:activity-ktx:$androidxActivity_version"
// Add a dependency on NitroModules
implementation project(":react-native-nitro-modules")
// For media playback using ExoPlayer
if (media3_buildFromSource) {
implementation(project(":media-lib-exoplayer"))
} else {
implementation "androidx.media3:media3-exoplayer:$media3_version"
}
implementation "androidx.core:core-ktx:1.13.1"
implementation "androidx.activity:activity-ktx:1.8.2"
if (ExoplayerDependencies["useExoplayerSmoothStreaming"]) {
// For Smooth Streaming playback support with ExoPlayer
if (media3_buildFromSource) {
implementation(project(":media-lib-exoplayer-smoothstreaming"))
} else {
implementation "androidx.media3:media3-exoplayer-smoothstreaming:$media3_version"
}
}
// For media playback using ExoPlayer
implementation "androidx.media3:media3-exoplayer:1.4.1"
if (ExoplayerDependencies["useExoplayerDash"]) {
// For DASH playback support with ExoPlayer
if (media3_buildFromSource) {
implementation(project(":media-lib-exoplayer-dash"))
} else {
implementation "androidx.media3:media3-exoplayer-dash:$media3_version"
}
}
// Common functionality used across multiple media libraries
implementation "androidx.media3:media3-common:1.4.1"
if (ExoplayerDependencies["useExoplayerHls"]) {
// For HLS playback support with ExoPlayer
if (media3_buildFromSource) {
implementation(project(":media-lib-exoplayer-hls"))
} else {
implementation "androidx.media3:media3-exoplayer-hls:$media3_version"
}
}
// Hls
implementation("androidx.media3:media3-exoplayer-hls:1.4.1")
// For RTSP playback support with ExoPlayer
if (ExoplayerDependencies["useExoplayerRtsp"]) {
if (media3_buildFromSource) {
implementation(project(":media-lib-exoplayer-rtsp"))
} else {
implementation "androidx.media3:media3-exoplayer-rtsp:$media3_version"
}
}
// For building media playback UIs
implementation "androidx.media3:media3-ui:1.4.1"
}
// For ad insertion using the Interactive Media Ads SDK with ExoPlayer
if (ExoplayerDependencies["useExoplayerIMA"]) {
if (media3_buildFromSource) {
implementation(project(":media-lib-exoplayer-ima"))
} else {
implementation "androidx.media3:media3-exoplayer-ima:$media3_version"
}
}
if (isNewArchitectureEnabled()) {
react {
jsRootDir = file("../src/spec/fabric")
libraryName = "VideoView"
codegenJavaPackageName = "com.video"
}
}
if (media3_buildFromSource) {
// For loading data using the OkHttp network stack
implementation(project(":media-lib-datasource-okhttp"))
// For building media playback UIs
implementation(project(":media-lib-ui"))
// For exposing and controlling media sessions
implementation(project(":media-lib-session"))
// Common functionality for loading data
implementation(project(":media-lib-datasource"))
// Common functionality used across multiple media libraries
implementation(project(":media-lib-common"))
} else {
// For loading data using the OkHttp network stack
implementation "androidx.media3:media3-datasource-okhttp:$media3_version"
// For building media playback UIs
implementation "androidx.media3:media3-ui:$media3_version"
// For exposing and controlling media sessions
implementation "androidx.media3:media3-session:$media3_version"
// Common functionality for loading data
implementation "androidx.media3:media3-datasource:$media3_version"
// Common functionality used across multiple media libraries
implementation "androidx.media3:media3-common:$media3_version"
}
}

View File

@@ -1,15 +1,5 @@
RNVideo_kotlinVersion=1.8.0
RNVideo_minSdkVersion=23
RNVideo_targetSdkVersion=34
RNVideo_compileSdkVersion=34
RNVideo_ndkversion=26.1.10909125
RNVideo_buildToolsVersion=34.0.0
RNVideo_media3Version=1.4.1
RNVideo_useExoplayerIMA=false
RNVideo_useExoplayerRtsp=false
RNVideo_useExoplayerSmoothStreaming=true
RNVideo_useExoplayerDash=true
RNVideo_useExoplayerHls=true
RNVideo_androidxCoreVersion=1.13.1
RNVideo_androidxActivityVersion=1.9.3
RNVideo_buildFromMedia3Source=false
Video_kotlinVersion=1.9.24
Video_minSdkVersion=23
Video_targetSdkVersion=34
Video_compileSdkVersion=34
Video_ndkversion=26.1.10909125

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<lint>
<issue id="UnsafeOptInUsageError">
<ignore regexp='\(markerClass = androidx\.media3\.common\.util\.UnstableApi\.class\)' />
</issue>
</lint>

View File

@@ -1,3 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.brentvatne.react">
package="com.video">
</manifest>

View File

@@ -0,0 +1,6 @@
#include <jni.h>
#include "NitroVideoOnLoad.hpp"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
return margelo::nitro::video::initialize(vm);
}

View File

@@ -1,36 +0,0 @@
package androidx.media3.exoplayer.dash;
import androidx.media3.common.MediaItem;
import androidx.media3.datasource.DataSource;
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
public class DashMediaSource {
public static class Factory implements MediaSource.Factory {
public Factory(DefaultDashChunkSource.Factory factory, DataSource.Factory factory1) {
}
@Override
public MediaSource.Factory setDrmSessionManagerProvider(DrmSessionManagerProvider drmSessionManagerProvider) {
return null;
}
@Override
public MediaSource.Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
return null;
}
@Override
public int[] getSupportedTypes() {
return new int[0];
}
@Override
public MediaSource createMediaSource(MediaItem mediaItem) {
return null;
}
}
}

View File

@@ -1,12 +0,0 @@
package androidx.media3.exoplayer.dash;
import android.net.Uri;
import androidx.media3.datasource.DataSource;
import androidx.media3.exoplayer.dash.manifest.DashManifest;
public class DashUtil {
public static DashManifest loadManifest(DataSource ds, Uri uri) {
return null;
}
}

View File

@@ -1,10 +0,0 @@
package androidx.media3.exoplayer.dash;
import androidx.media3.datasource.DataSource;
public class DefaultDashChunkSource {
public static final class Factory {
public Factory(DataSource.Factory mediaDataSourceFactory) {
}
}
}

View File

@@ -1,13 +0,0 @@
package androidx.media3.exoplayer.dash.manifest;
import androidx.collection.CircularArray;
import androidx.media3.common.C;
public class AdaptationSet {
public int type = 0;
public CircularArray<Representation> representations;
public AdaptationSet() {
representations = null;
}
}

View File

@@ -1,15 +0,0 @@
package androidx.media3.exoplayer.dash.manifest;
public class DashManifest {
public DashManifest() {
}
public int getPeriodCount() {
return 0;
}
public Period getPeriod(int index) {
return null;
}
}

View File

@@ -1,7 +0,0 @@
package androidx.media3.exoplayer.dash.manifest;
import androidx.collection.CircularArray;
public class Period {
public CircularArray<AdaptationSet> adaptationSets;
}

View File

@@ -1,12 +0,0 @@
package androidx.media3.exoplayer.dash.manifest;
import androidx.media3.common.Format;
public class Representation {
public Format format;
public long presentationTimeOffsetUs;
public Representation() {
format = null;
}
}

View File

@@ -1,38 +0,0 @@
package androidx.media3.exoplayer.hls;
import androidx.media3.common.MediaItem;
import androidx.media3.datasource.DataSource;
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
public class HlsMediaSource {
public static class Factory implements MediaSource.Factory {
public Factory(DataSource.Factory mediaDataSourceFactory) {
}
@Override
public MediaSource.Factory setDrmSessionManagerProvider(DrmSessionManagerProvider drmSessionManagerProvider) {
return null;
}
@Override
public MediaSource.Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
return null;
}
@Override
public int[] getSupportedTypes() {
return new int[0];
}
@Override
public MediaSource createMediaSource(MediaItem mediaItem) {
return null;
}
public Factory setAllowChunklessPreparation(boolean allowChunklessPreparation) {
return this;
}
}
}

View File

@@ -1,77 +0,0 @@
package androidx.media3.exoplayer.ima;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.media3.common.AdViewProvider;
import androidx.media3.common.Player;
import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.source.ads.AdsLoader;
import androidx.media3.exoplayer.source.ads.AdsMediaSource;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import java.io.IOException;
public class ImaAdsLoader implements AdsLoader {
private final ImaSdkSettings imaSdkSettings;
public ImaAdsLoader(ImaSdkSettings imaSdkSettings) {
this.imaSdkSettings = imaSdkSettings;
}
public void setPlayer(ExoPlayer ignoredPlayer) {
}
@Override
public void setPlayer(@Nullable Player player) {
}
public void release() {
}
@Override
public void setSupportedContentTypes(@NonNull int... ints) {
}
@Override
public void start(@NonNull AdsMediaSource adsMediaSource, @NonNull DataSpec dataSpec, @NonNull Object adsId, @NonNull AdViewProvider adViewProvider, @NonNull EventListener eventListener) {
}
@Override
public void stop(@NonNull AdsMediaSource adsMediaSource, @NonNull EventListener eventListener) {
}
@Override
public void handlePrepareComplete(@NonNull AdsMediaSource adsMediaSource, int i, int i1) {
}
@Override
public void handlePrepareError(@NonNull AdsMediaSource adsMediaSource, int i, int i1, @NonNull IOException e) {
}
public static class Builder {
private ImaSdkSettings imaSdkSettings;
public Builder(Context ignoredThemedReactContext) {
}
public Builder setAdEventListener(Object ignoredReactExoplayerView) {
return this;
}
public Builder setAdErrorListener(Object ignoredReactExoplayerView) {
return this;
}
public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) {
this.imaSdkSettings = imaSdkSettings;
return this;
}
public ImaAdsLoader build() {
return null;
}
}
}

View File

@@ -1,34 +0,0 @@
package androidx.media3.exoplayer.rtsp;
import androidx.media3.common.MediaItem;
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
public class RtspMediaSource {
public RtspMediaSource() {
}
public static class Factory implements MediaSource.Factory {
@Override
public MediaSource.Factory setDrmSessionManagerProvider(DrmSessionManagerProvider drmSessionManagerProvider) {
return null;
}
@Override
public MediaSource.Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
return null;
}
@Override
public int[] getSupportedTypes() {
return new int[0];
}
@Override
public MediaSource createMediaSource(MediaItem mediaItem) {
return null;
}
}
}

View File

@@ -1,34 +0,0 @@
package androidx.media3.exoplayer.smoothstreaming;
import androidx.media3.common.MediaItem;
import androidx.media3.datasource.DataSource;
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
public class DefaultSsChunkSource {
public static class Factory implements MediaSource.Factory {
public Factory(DataSource.Factory mediaDataSourceFactory) {
}
@Override
public MediaSource.Factory setDrmSessionManagerProvider(DrmSessionManagerProvider drmSessionManagerProvider) {
return null;
}
@Override
public MediaSource.Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
return null;
}
@Override
public int[] getSupportedTypes() {
return new int[0];
}
@Override
public MediaSource createMediaSource(MediaItem mediaItem) {
return null;
}
}
}

View File

@@ -1,34 +0,0 @@
package androidx.media3.exoplayer.smoothstreaming;
import androidx.media3.common.MediaItem;
import androidx.media3.datasource.DataSource;
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
public class SsMediaSource {
public static class Factory implements MediaSource.Factory {
public Factory(DefaultSsChunkSource.Factory factory, DataSource.Factory factory1) {
}
@Override
public MediaSource.Factory setDrmSessionManagerProvider(DrmSessionManagerProvider drmSessionManagerProvider) {
return null;
}
@Override
public MediaSource.Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
return null;
}
@Override
public int[] getSupportedTypes() {
return new int[0];
}
@Override
public MediaSource createMediaSource(MediaItem mediaItem) {
return null;
}
}
}

View File

@@ -1,43 +0,0 @@
package com.brentvatne.common.api
import android.net.Uri
import android.text.TextUtils
import com.brentvatne.common.toolbox.ReactBridgeUtils
import com.facebook.react.bridge.ReadableMap
class AdsProps {
var adTagUrl: Uri? = null
var adLanguage: String? = null
/** return true if this and src are equals */
override fun equals(other: Any?): Boolean {
if (other == null || other !is AdsProps) return false
return (
adTagUrl == other.adTagUrl &&
adLanguage == other.adLanguage
)
}
companion object {
private const val PROP_AD_TAG_URL = "adTagUrl"
private const val PROP_AD_LANGUAGE = "adLanguage"
@JvmStatic
fun parse(src: ReadableMap?): AdsProps {
val adsProps = AdsProps()
if (src != null) {
val uriString = ReactBridgeUtils.safeGetString(src, PROP_AD_TAG_URL)
if (TextUtils.isEmpty(uriString)) {
adsProps.adTagUrl = null
} else {
adsProps.adTagUrl = Uri.parse(uriString)
}
val languageString = ReactBridgeUtils.safeGetString(src, PROP_AD_LANGUAGE)
if (!TextUtils.isEmpty(languageString)) {
adsProps.adLanguage = languageString
}
}
return adsProps
}
}
}

View File

@@ -1,130 +0,0 @@
package com.brentvatne.common.api
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetDouble
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetFloat
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetInt
import com.facebook.react.bridge.ReadableMap
/**
* Class representing bufferConfig for host.
* Only generic code here, no reference to the player.
* By default, if application don't provide input field, -1 is set instead
*/
class BufferConfig {
var cacheSize = BufferConfigPropUnsetInt
var minBufferMs = BufferConfigPropUnsetInt
var maxBufferMs = BufferConfigPropUnsetInt
var bufferForPlaybackMs = BufferConfigPropUnsetInt
var bufferForPlaybackAfterRebufferMs = BufferConfigPropUnsetInt
var backBufferDurationMs = BufferConfigPropUnsetInt
var maxHeapAllocationPercent = BufferConfigPropUnsetDouble
var minBackBufferMemoryReservePercent = BufferConfigPropUnsetDouble
var minBufferMemoryReservePercent = BufferConfigPropUnsetDouble
var initialBitrate = BufferConfigPropUnsetInt
var live: Live = Live()
/** return true if this and src are equals */
override fun equals(other: Any?): Boolean {
if (other == null || other !is BufferConfig) return false
return (
cacheSize == other.cacheSize &&
minBufferMs == other.minBufferMs &&
maxBufferMs == other.maxBufferMs &&
bufferForPlaybackMs == other.bufferForPlaybackMs &&
bufferForPlaybackAfterRebufferMs == other.bufferForPlaybackAfterRebufferMs &&
backBufferDurationMs == other.backBufferDurationMs &&
maxHeapAllocationPercent == other.maxHeapAllocationPercent &&
minBackBufferMemoryReservePercent == other.minBackBufferMemoryReservePercent &&
minBufferMemoryReservePercent == other.minBufferMemoryReservePercent &&
initialBitrate == other.initialBitrate &&
live == other.live
)
}
class Live {
var maxPlaybackSpeed: Float = BufferConfigPropUnsetDouble.toFloat()
var minPlaybackSpeed: Float = BufferConfigPropUnsetDouble.toFloat()
var maxOffsetMs: Long = BufferConfigPropUnsetInt.toLong()
var minOffsetMs: Long = BufferConfigPropUnsetInt.toLong()
var targetOffsetMs: Long = BufferConfigPropUnsetInt.toLong()
override fun equals(other: Any?): Boolean {
if (other == null || other !is Live) return false
return (
maxPlaybackSpeed == other.maxPlaybackSpeed &&
minPlaybackSpeed == other.minPlaybackSpeed &&
maxOffsetMs == other.maxOffsetMs &&
minOffsetMs == other.minOffsetMs &&
targetOffsetMs == other.targetOffsetMs
)
}
companion object {
private const val PROP_BUFFER_CONFIG_LIVE_MAX_PLAYBACK_SPEED = "maxPlaybackSpeed"
private const val PROP_BUFFER_CONFIG_LIVE_MIN_PLAYBACK_SPEED = "minPlaybackSpeed"
private const val PROP_BUFFER_CONFIG_LIVE_MAX_OFFSET_MS = "maxOffsetMs"
private const val PROP_BUFFER_CONFIG_LIVE_MIN_OFFSET_MS = "minOffsetMs"
private const val PROP_BUFFER_CONFIG_LIVE_TARGET_OFFSET_MS = "targetOffsetMs"
@JvmStatic
fun parse(src: ReadableMap?): Live {
val live = Live()
live.maxPlaybackSpeed = safeGetFloat(src, PROP_BUFFER_CONFIG_LIVE_MAX_PLAYBACK_SPEED, BufferConfigPropUnsetDouble.toFloat())
live.minPlaybackSpeed = safeGetFloat(src, PROP_BUFFER_CONFIG_LIVE_MIN_PLAYBACK_SPEED, BufferConfigPropUnsetDouble.toFloat())
live.maxOffsetMs = safeGetInt(src, PROP_BUFFER_CONFIG_LIVE_MAX_OFFSET_MS, BufferConfigPropUnsetInt).toLong()
live.minOffsetMs = safeGetInt(src, PROP_BUFFER_CONFIG_LIVE_MIN_OFFSET_MS, BufferConfigPropUnsetInt).toLong()
live.targetOffsetMs = safeGetInt(src, PROP_BUFFER_CONFIG_LIVE_TARGET_OFFSET_MS, BufferConfigPropUnsetInt).toLong()
return live
}
}
}
companion object {
val BufferConfigPropUnsetInt = -1
val BufferConfigPropUnsetDouble = -1.0
private const val PROP_BUFFER_CONFIG_CACHE_SIZE = "cacheSizeMB"
private const val PROP_BUFFER_CONFIG_MIN_BUFFER_MS = "minBufferMs"
private const val PROP_BUFFER_CONFIG_MAX_BUFFER_MS = "maxBufferMs"
private const val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs"
private const val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = "bufferForPlaybackAfterRebufferMs"
private const val PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT = "maxHeapAllocationPercent"
private const val PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT = "minBackBufferMemoryReservePercent"
private const val PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT = "minBufferMemoryReservePercent"
private const val PROP_BUFFER_CONFIG_BACK_BUFFER_DURATION_MS = "backBufferDurationMs"
private const val PROP_BUFFER_CONFIG_INITIAL_BITRATE = "initialBitrate"
private const val PROP_BUFFER_CONFIG_LIVE = "live"
@JvmStatic
fun parse(src: ReadableMap?): BufferConfig {
val bufferConfig = BufferConfig()
if (src != null) {
bufferConfig.cacheSize = safeGetInt(src, PROP_BUFFER_CONFIG_CACHE_SIZE, BufferConfigPropUnsetInt)
bufferConfig.minBufferMs = safeGetInt(src, PROP_BUFFER_CONFIG_MIN_BUFFER_MS, BufferConfigPropUnsetInt)
bufferConfig.maxBufferMs = safeGetInt(src, PROP_BUFFER_CONFIG_MAX_BUFFER_MS, BufferConfigPropUnsetInt)
bufferConfig.bufferForPlaybackMs = safeGetInt(src, PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS, BufferConfigPropUnsetInt)
bufferConfig.bufferForPlaybackAfterRebufferMs =
safeGetInt(src, PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, BufferConfigPropUnsetInt)
bufferConfig.maxHeapAllocationPercent =
safeGetDouble(src, PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT, BufferConfigPropUnsetDouble)
bufferConfig.minBackBufferMemoryReservePercent = safeGetDouble(
src,
PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT,
BufferConfigPropUnsetDouble
)
bufferConfig.minBufferMemoryReservePercent =
safeGetDouble(
src,
PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT,
BufferConfigPropUnsetDouble
)
bufferConfig.backBufferDurationMs = safeGetInt(src, PROP_BUFFER_CONFIG_BACK_BUFFER_DURATION_MS, BufferConfigPropUnsetInt)
bufferConfig.initialBitrate = safeGetInt(src, PROP_BUFFER_CONFIG_INITIAL_BITRATE, BufferConfigPropUnsetInt)
bufferConfig.live = Live.parse(src.getMap(PROP_BUFFER_CONFIG_LIVE))
}
return bufferConfig
}
}
}

View File

@@ -1,47 +0,0 @@
package com.brentvatne.common.api
import com.brentvatne.common.toolbox.DebugLog
/**
* Define how exoplayer with load data and parsing helper
*/
class BufferingStrategy {
/**
* Define how exoplayer with load data
*/
enum class BufferingStrategyEnum {
/**
* default exoplayer strategy
*/
Default,
/**
* never load more than needed
*/
DisableBuffering,
/**
* use default strategy but pause loading when available memory is low
*/
DependingOnMemory
}
companion object {
private const val TAG = "BufferingStrategy"
/**
* companion function to transform input string to enum
*/
fun parse(src: String?): BufferingStrategyEnum {
if (src == null) return BufferingStrategyEnum.Default
return try {
BufferingStrategyEnum.valueOf(src)
} catch (e: Exception) {
DebugLog.e(TAG, "cannot parse buffering strategy " + src)
BufferingStrategyEnum.Default
}
}
}
}

View File

@@ -1,51 +0,0 @@
package com.brentvatne.common.api
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetInt
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.ReadableType
data class CMCDProps(
val cmcdObject: List<Pair<String, Any>> = emptyList(),
val cmcdRequest: List<Pair<String, Any>> = emptyList(),
val cmcdSession: List<Pair<String, Any>> = emptyList(),
val cmcdStatus: List<Pair<String, Any>> = emptyList(),
val mode: Int = 1
) {
companion object {
private const val PROP_CMCD_OBJECT = "object"
private const val PROP_CMCD_REQUEST = "request"
private const val PROP_CMCD_SESSION = "session"
private const val PROP_CMCD_STATUS = "status"
private const val PROP_CMCD_MODE = "mode"
@JvmStatic
fun parse(src: ReadableMap?): CMCDProps? {
if (src == null) return null
return CMCDProps(
cmcdObject = parseKeyValuePairs(src.getArray(PROP_CMCD_OBJECT)),
cmcdRequest = parseKeyValuePairs(src.getArray(PROP_CMCD_REQUEST)),
cmcdSession = parseKeyValuePairs(src.getArray(PROP_CMCD_SESSION)),
cmcdStatus = parseKeyValuePairs(src.getArray(PROP_CMCD_STATUS)),
mode = safeGetInt(src, PROP_CMCD_MODE, 1)
)
}
private fun parseKeyValuePairs(array: ReadableArray?): List<Pair<String, Any>> {
if (array == null) return emptyList()
return (0 until array.size()).mapNotNull { i ->
val item = array.getMap(i)
val key = item?.getString("key")
val value = when (item?.getType("value")) {
ReadableType.Number -> item?.getDouble("value")
ReadableType.String -> item?.getString("value")
else -> null
}
if (key != null && value != null) Pair(key, value) else null
}
}
}
}

View File

@@ -1,47 +0,0 @@
package com.brentvatne.common.api
import com.brentvatne.common.toolbox.ReactBridgeUtils
import com.facebook.react.bridge.ReadableMap
class ControlsConfig {
var hideSeekBar: Boolean = false
var hideDuration: Boolean = false
var hidePosition: Boolean = false
var hidePlayPause: Boolean = false
var hideForward: Boolean = false
var hideRewind: Boolean = false
var hideNext: Boolean = false
var hidePrevious: Boolean = false
var hideFullscreen: Boolean = false
var hideNavigationBarOnFullScreenMode: Boolean = true
var hideNotificationBarOnFullScreenMode: Boolean = true
var liveLabel: String? = null
var hideSettingButton: Boolean = true
var seekIncrementMS: Int = 10000
companion object {
@JvmStatic
fun parse(controlsConfig: ReadableMap?): ControlsConfig {
val config = ControlsConfig()
if (controlsConfig != null) {
config.hideSeekBar = ReactBridgeUtils.safeGetBool(controlsConfig, "hideSeekBar", false)
config.hideDuration = ReactBridgeUtils.safeGetBool(controlsConfig, "hideDuration", false)
config.hidePosition = ReactBridgeUtils.safeGetBool(controlsConfig, "hidePosition", false)
config.hidePlayPause = ReactBridgeUtils.safeGetBool(controlsConfig, "hidePlayPause", false)
config.hideForward = ReactBridgeUtils.safeGetBool(controlsConfig, "hideForward", false)
config.hideRewind = ReactBridgeUtils.safeGetBool(controlsConfig, "hideRewind", false)
config.hideNext = ReactBridgeUtils.safeGetBool(controlsConfig, "hideNext", false)
config.hidePrevious = ReactBridgeUtils.safeGetBool(controlsConfig, "hidePrevious", false)
config.hideFullscreen = ReactBridgeUtils.safeGetBool(controlsConfig, "hideFullscreen", false)
config.seekIncrementMS = ReactBridgeUtils.safeGetInt(controlsConfig, "seekIncrementMS", 10000)
config.hideNavigationBarOnFullScreenMode = ReactBridgeUtils.safeGetBool(controlsConfig, "hideNavigationBarOnFullScreenMode", true)
config.hideNotificationBarOnFullScreenMode = ReactBridgeUtils.safeGetBool(controlsConfig, "hideNotificationBarOnFullScreenMode", true)
config.liveLabel = ReactBridgeUtils.safeGetString(controlsConfig, "liveLabel", null)
config.hideSettingButton = ReactBridgeUtils.safeGetBool(controlsConfig, "hideSettingButton", true)
}
return config
}
}
}

View File

@@ -1,84 +0,0 @@
package com.brentvatne.common.api
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetArray
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetBool
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetString
import com.facebook.react.bridge.ReadableMap
import java.util.UUID
/**
* Class representing DRM props for host.
* Only generic code here, no reference to the player.
*/
class DRMProps {
/**
* string version of configured UUID for drm prop
*/
var drmType: String? = null
/**
* Configured UUID for drm prop
*/
var drmUUID: UUID? = null
/**
* DRM license server to be used
*/
var drmLicenseServer: String? = null
/**
* DRM Http Header to access to license server
*/
var drmLicenseHeader: Array<String> = emptyArray<String>()
/**
* Flag to enable key rotation support
*/
var multiDrm: Boolean = false
/** return true if this and src are equals */
override fun equals(other: Any?): Boolean {
if (other == null || other !is DRMProps) return false
return drmType == other.drmType &&
drmLicenseServer == other.drmLicenseServer &&
multiDrm == other.multiDrm &&
drmLicenseHeader.contentDeepEquals(other.drmLicenseHeader) // drmLicenseHeader is never null
}
companion object {
private const val PROP_DRM_TYPE = "type"
private const val PROP_DRM_LICENSE_SERVER = "licenseServer"
private const val PROP_DRM_HEADERS = "headers"
private const val PROP_DRM_HEADERS_KEY = "key"
private const val PROP_DRM_HEADERS_VALUE = "value"
private const val PROP_DRM_MULTI_DRM = "multiDrm"
/** parse the source ReadableMap received from app */
@JvmStatic
fun parse(src: ReadableMap?): DRMProps? {
var drm: DRMProps? = null
if (src != null && src.hasKey(PROP_DRM_TYPE)) {
drm = DRMProps()
drm.drmType = safeGetString(src, PROP_DRM_TYPE)
drm.drmLicenseServer = safeGetString(src, PROP_DRM_LICENSE_SERVER)
drm.multiDrm = safeGetBool(src, PROP_DRM_MULTI_DRM, false)
val drmHeadersArray = safeGetArray(src, PROP_DRM_HEADERS)
if (drm.drmType != null && drm.drmLicenseServer != null) {
if (drmHeadersArray != null) {
val drmKeyRequestPropertiesList = ArrayList<String?>()
for (i in 0 until drmHeadersArray.size()) {
val current = drmHeadersArray.getMap(i)
drmKeyRequestPropertiesList.add(safeGetString(current, PROP_DRM_HEADERS_KEY))
drmKeyRequestPropertiesList.add(safeGetString(current, PROP_DRM_HEADERS_VALUE))
}
val array = emptyArray<String>()
drm.drmLicenseHeader = drmKeyRequestPropertiesList.toArray(array)
}
} else {
return null
}
}
return drm
}
}
}

View File

@@ -1,53 +0,0 @@
package com.brentvatne.common.api
import androidx.annotation.IntDef
import kotlin.annotation.Retention
internal object ResizeMode {
/**
* Either the width or height is decreased to obtain the desired aspect ratio.
*/
const val RESIZE_MODE_FIT = 0
/**
* The width is fixed and the height is increased or decreased to obtain the desired aspect ratio.
*/
const val RESIZE_MODE_FIXED_WIDTH = 1
/**
* The height is fixed and the width is increased or decreased to obtain the desired aspect ratio.
*/
const val RESIZE_MODE_FIXED_HEIGHT = 2
/**
* The height and the width is increased or decreased to fit the size of the view.
*/
const val RESIZE_MODE_FILL = 3
/**
* Keeps the aspect ratio but takes up the view's size.
*/
const val RESIZE_MODE_CENTER_CROP = 4
@JvmStatic
@Mode
fun toResizeMode(ordinal: Int): Int =
when (ordinal) {
RESIZE_MODE_FIXED_WIDTH -> RESIZE_MODE_FIXED_WIDTH
RESIZE_MODE_FIXED_HEIGHT -> RESIZE_MODE_FIXED_HEIGHT
RESIZE_MODE_FILL -> RESIZE_MODE_FILL
RESIZE_MODE_CENTER_CROP -> RESIZE_MODE_CENTER_CROP
RESIZE_MODE_FIT -> RESIZE_MODE_FIT
else -> RESIZE_MODE_FIT
}
@Retention(AnnotationRetention.SOURCE)
@IntDef(
RESIZE_MODE_FIT,
RESIZE_MODE_FIXED_WIDTH,
RESIZE_MODE_FIXED_HEIGHT,
RESIZE_MODE_FILL,
RESIZE_MODE_CENTER_CROP
)
annotation class Mode
}

View File

@@ -1,34 +0,0 @@
package com.brentvatne.common.api
import android.net.Uri
import com.brentvatne.common.toolbox.ReactBridgeUtils
import com.facebook.react.bridge.ReadableMap
/**
* Class representing a sideLoaded text track from application
* Do you use player import in this class
*/
class SideLoadedTextTrack {
var language: String? = null
var title: String? = null
var uri: Uri = Uri.EMPTY
var type: String? = null
companion object {
val SIDELOAD_TEXT_TRACK_LANGUAGE = "language"
val SIDELOAD_TEXT_TRACK_TITLE = "title"
val SIDELOAD_TEXT_TRACK_URI = "uri"
val SIDELOAD_TEXT_TRACK_TYPE = "type"
fun parse(src: ReadableMap?): SideLoadedTextTrack {
val sideLoadedTextTrack = SideLoadedTextTrack()
if (src == null) {
return sideLoadedTextTrack
}
sideLoadedTextTrack.language = ReactBridgeUtils.safeGetString(src, SIDELOAD_TEXT_TRACK_LANGUAGE)
sideLoadedTextTrack.title = ReactBridgeUtils.safeGetString(src, SIDELOAD_TEXT_TRACK_TITLE, "")
sideLoadedTextTrack.uri = Uri.parse(ReactBridgeUtils.safeGetString(src, SIDELOAD_TEXT_TRACK_URI, ""))
sideLoadedTextTrack.type = ReactBridgeUtils.safeGetString(src, SIDELOAD_TEXT_TRACK_TYPE, "")
return sideLoadedTextTrack
}
}
}

View File

@@ -1,35 +0,0 @@
package com.brentvatne.common.api
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
/**
* Class representing a list of sideLoaded text track from application
* Do you use player import in this class
*/
class SideLoadedTextTrackList {
var tracks = ArrayList<SideLoadedTextTrack>()
/** return true if this and src are equals */
override fun equals(other: Any?): Boolean {
if (other == null || other !is SideLoadedTextTrackList) return false
return tracks == other.tracks
}
companion object {
fun parse(src: ReadableArray?): SideLoadedTextTrackList? {
if (src == null) {
return null
}
val sideLoadedTextTrackList = SideLoadedTextTrackList()
for (i in 0 until src.size()) {
val textTrack: ReadableMap? = src.getMap(i)
textTrack?.let {
sideLoadedTextTrackList.tracks.add(SideLoadedTextTrack.parse(it))
}
}
return sideLoadedTextTrackList
}
}
}

View File

@@ -1,287 +0,0 @@
package com.brentvatne.common.api
import android.annotation.SuppressLint
import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources
import android.net.Uri
import android.text.TextUtils
import com.brentvatne.common.api.DRMProps.Companion.parse
import com.brentvatne.common.toolbox.DebugLog
import com.brentvatne.common.toolbox.DebugLog.e
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetArray
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetBool
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetInt
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetMap
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetString
import com.brentvatne.react.BuildConfig
import com.facebook.react.bridge.ReadableMap
import java.util.Locale
import java.util.Objects
/**
* Class representing Source props for host.
* Only generic code here, no reference to the player.
*/
class Source {
/** String value of source to playback */
private var uriString: String? = null
/** Parsed value of source to playback */
var uri: Uri? = null
/** True if source is a local JS asset */
var isLocalAssetFile: Boolean = false
/** True if source is a local file asset://, ... */
var isAsset: Boolean = false
/** Start position of playback used to resume playback */
var startPositionMs: Int = -1
/** Will crop content start at specified position */
var cropStartMs: Int = -1
/** Will crop content end at specified position */
var cropEndMs: Int = -1
/** Will virtually consider that content before contentStartTime is a preroll ad */
var contentStartTime: Int = -1
/** Allow to force stream content, necessary when uri doesn't contain content type (.mlp4, .m3u, ...) */
var extension: String? = null
/** Metadata to display in notification */
var metadata: Metadata? = null
/** Allowed reload before failure notification */
var minLoadRetryCount = 3
/** http header list */
val headers: MutableMap<String, String> = HashMap()
/**
* DRM properties linked to the source
*/
var drmProps: DRMProps? = null
/** enable chunkless preparation for HLS
* see:
*/
var textTracksAllowChunklessPreparation: Boolean = false
/**
* CMCD properties linked to the source
*/
var cmcdProps: CMCDProps? = null
/**
* Ads playback properties
*/
var adsProps: AdsProps? = null
/*
* buffering configuration
*/
var bufferConfig = BufferConfig()
/**
* The list of sideLoaded text tracks
*/
var sideLoadedTextTracks: SideLoadedTextTrackList? = null
override fun hashCode(): Int = Objects.hash(uriString, uri, startPositionMs, cropStartMs, cropEndMs, extension, metadata, headers)
/** return true if this and src are equals */
override fun equals(other: Any?): Boolean {
if (other == null || other !is Source) return false
return (
uri == other.uri &&
cropStartMs == other.cropStartMs &&
cropEndMs == other.cropEndMs &&
startPositionMs == other.startPositionMs &&
extension == other.extension &&
drmProps == other.drmProps &&
contentStartTime == other.contentStartTime &&
cmcdProps == other.cmcdProps &&
sideLoadedTextTracks == other.sideLoadedTextTracks &&
adsProps == other.adsProps &&
minLoadRetryCount == other.minLoadRetryCount &&
isLocalAssetFile == other.isLocalAssetFile &&
isAsset == other.isAsset &&
bufferConfig == other.bufferConfig
)
}
/** return true if this and src are equals */
fun isEquals(source: Source): Boolean = this == source
/** Metadata to display in notification */
class Metadata {
/** Metadata title */
var title: String? = null
/** Metadata subtitle */
var subtitle: String? = null
/** Metadata description */
var description: String? = null
/** Metadata artist */
var artist: String? = null
/** image uri to display */
var imageUri: Uri? = null
companion object {
private const val PROP_SRC_METADATA_TITLE = "title"
private const val PROP_SRC_METADATA_SUBTITLE = "subtitle"
private const val PROP_SRC_METADATA_DESCRIPTION = "description"
private const val PROP_SRC_METADATA_ARTIST = "artist"
private const val PROP_SRC_METADATA_IMAGE_URI = "imageUri"
/** parse metadata object */
@JvmStatic
fun parse(src: ReadableMap?): Metadata? {
if (src != null) {
val metadata = Metadata()
metadata.title = safeGetString(src, PROP_SRC_METADATA_TITLE)
metadata.subtitle = safeGetString(src, PROP_SRC_METADATA_SUBTITLE)
metadata.description = safeGetString(src, PROP_SRC_METADATA_DESCRIPTION)
metadata.artist = safeGetString(src, PROP_SRC_METADATA_ARTIST)
val imageUriString = safeGetString(src, PROP_SRC_METADATA_IMAGE_URI)
try {
metadata.imageUri = Uri.parse(imageUriString)
} catch (e: Exception) {
e(TAG, "Could not parse imageUri in metadata")
}
return metadata
}
return null
}
}
}
companion object {
private const val TAG = "Source"
private const val PROP_SRC_URI = "uri"
private const val PROP_SRC_IS_LOCAL_ASSET_FILE = "isLocalAssetFile"
private const val PROP_SRC_IS_ASSET = "isAsset"
private const val PROP_SRC_START_POSITION = "startPosition"
private const val PROP_SRC_CROP_START = "cropStart"
private const val PROP_SRC_CROP_END = "cropEnd"
private const val PROP_SRC_CONTENT_START_TIME = "contentStartTime"
private const val PROP_SRC_TYPE = "type"
private const val PROP_SRC_METADATA = "metadata"
private const val PROP_SRC_HEADERS = "requestHeaders"
private const val PROP_SRC_DRM = "drm"
private const val PROP_SRC_CMCD = "cmcd"
private const val PROP_SRC_ADS = "ad"
private const val PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION = "textTracksAllowChunklessPreparation"
private const val PROP_SRC_TEXT_TRACKS = "textTracks"
private const val PROP_SRC_MIN_LOAD_RETRY_COUNT = "minLoadRetryCount"
private const val PROP_SRC_BUFFER_CONFIG = "bufferConfig"
@SuppressLint("DiscouragedApi")
private fun getUriFromAssetId(context: Context, uriString: String): Uri? {
val resources: Resources = context.resources
val packageName: String = context.packageName
var identifier = resources.getIdentifier(
uriString,
"drawable",
packageName
)
if (identifier == 0) {
identifier = resources.getIdentifier(
uriString,
"raw",
packageName
)
}
if (identifier <= 0) {
// cannot find identifier of content
DebugLog.d(TAG, "cannot find identifier")
return null
}
return Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE).path(identifier.toString()).build()
}
/** parse the source ReadableMap received from app */
@JvmStatic
fun parse(src: ReadableMap?, context: Context): Source {
val source = Source()
if (src != null) {
val uriString = safeGetString(src, PROP_SRC_URI, null)
if (uriString == null || TextUtils.isEmpty(uriString)) {
DebugLog.d(TAG, "isEmpty uri:$uriString")
return source
}
var uri = Uri.parse(uriString)
if (uri == null) {
// return an empty source
DebugLog.d(TAG, "Invalid uri:$uriString")
return source
} else if (!isValidScheme(uri.scheme)) {
uri = getUriFromAssetId(context, uriString)
if (uri == null) {
// cannot find identifier of content
DebugLog.d(TAG, "cannot find identifier")
return source
}
}
source.uriString = uriString
source.uri = uri
source.isLocalAssetFile = safeGetBool(src, PROP_SRC_IS_LOCAL_ASSET_FILE, false)
source.isAsset = safeGetBool(src, PROP_SRC_IS_ASSET, false)
source.startPositionMs = safeGetInt(src, PROP_SRC_START_POSITION, -1)
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))
}
return source
}
/** return true if rui scheme is supported for android playback */
private fun isValidScheme(scheme: String?): Boolean {
if (scheme == null) {
return false
}
val lowerCaseUri = scheme.lowercase(Locale.getDefault())
return (
lowerCaseUri == "http" ||
lowerCaseUri == "https" ||
lowerCaseUri == "content" ||
lowerCaseUri == "file" ||
lowerCaseUri == "rtsp" ||
lowerCaseUri == "asset"
)
}
}
}

View File

@@ -1,47 +0,0 @@
package com.brentvatne.common.api
import com.brentvatne.common.toolbox.ReactBridgeUtils
import com.facebook.react.bridge.ReadableMap
/**
* Helper file to parse SubtitleStyle prop and build a dedicated class
*/
class SubtitleStyle public constructor() {
var fontSize = -1
private set
var paddingLeft = 0
private set
var paddingRight = 0
private set
var paddingTop = 0
private set
var paddingBottom = 0
private set
var opacity = 1f
private set
var subtitlesFollowVideo = true
private set
companion object {
private const val PROP_FONT_SIZE_TRACK = "fontSize"
private const val PROP_PADDING_BOTTOM = "paddingBottom"
private const val PROP_PADDING_TOP = "paddingTop"
private const val PROP_PADDING_LEFT = "paddingLeft"
private const val PROP_PADDING_RIGHT = "paddingRight"
private const val PROP_OPACITY = "opacity"
private const val PROP_SUBTITLES_FOLLOW_VIDEO = "subtitlesFollowVideo"
@JvmStatic
fun parse(src: ReadableMap?): SubtitleStyle {
val subtitleStyle = SubtitleStyle()
subtitleStyle.fontSize = ReactBridgeUtils.safeGetInt(src, PROP_FONT_SIZE_TRACK, -1)
subtitleStyle.paddingBottom = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_BOTTOM, 0)
subtitleStyle.paddingTop = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_TOP, 0)
subtitleStyle.paddingLeft = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_LEFT, 0)
subtitleStyle.paddingRight = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_RIGHT, 0)
subtitleStyle.opacity = ReactBridgeUtils.safeGetFloat(src, PROP_OPACITY, 1f)
subtitleStyle.subtitlesFollowVideo = ReactBridgeUtils.safeGetBool(src, PROP_SUBTITLES_FOLLOW_VIDEO, true)
return subtitleStyle
}
}
}

View File

@@ -1,10 +0,0 @@
package com.brentvatne.common.api
/*
* class to handle timedEvent retrieved from the stream
*/
class TimedMetadata(_identifier: String? = null, _value: String? = null) {
var identifier: String? = _identifier
var value: String? = _value
}

View File

@@ -1,15 +0,0 @@
package com.brentvatne.common.api
/*
* internal representation of audio & text tracks
*/
class Track {
var title: String? = null
var mimeType: String? = null
var language: String? = null
var isSelected = false
// in bps available only on audio tracks
var bitrate = 0
var index = 0
}

View File

@@ -1,16 +0,0 @@
package com.brentvatne.common.api
/*
* internal representation of audio & text tracks
*/
class VideoTrack {
var width = 0
var height = 0
var bitrate = 0
var codecs = ""
var index = -1
var trackId = ""
var isSelected = false
var rotation = 0
}

View File

@@ -1,19 +0,0 @@
package com.brentvatne.common.api
internal object ViewType {
/**
* View used will be a TextureView.
*/
const val VIEW_TYPE_TEXTURE = 0
/**
* View used will be a SurfaceView.
*/
const val VIEW_TYPE_SURFACE = 1
/**
* View used will be a SurfaceView with secure flag set.
*/
const val VIEW_TYPE_SURFACE_SECURE = 2
annotation class ViewType
}

View File

@@ -1,370 +0,0 @@
package com.brentvatne.common.react
import com.brentvatne.common.api.TimedMetadata
import com.brentvatne.common.api.Track
import com.brentvatne.common.api.VideoTrack
import com.brentvatne.exoplayer.ReactExoplayerView
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.EventDispatcher
import java.io.PrintWriter
import java.io.StringWriter
enum class EventTypes(val eventName: String) {
EVENT_LOAD_START("onVideoLoadStart"),
EVENT_LOAD("onVideoLoad"),
EVENT_ERROR("onVideoError"),
EVENT_PROGRESS("onVideoProgress"),
EVENT_BANDWIDTH("onVideoBandwidthUpdate"),
EVENT_CONTROLS_VISIBILITY_CHANGE("onControlsVisibilityChange"),
EVENT_SEEK("onVideoSeek"),
EVENT_END("onVideoEnd"),
EVENT_FULLSCREEN_WILL_PRESENT("onVideoFullscreenPlayerWillPresent"),
EVENT_FULLSCREEN_DID_PRESENT("onVideoFullscreenPlayerDidPresent"),
EVENT_FULLSCREEN_WILL_DISMISS("onVideoFullscreenPlayerWillDismiss"),
EVENT_FULLSCREEN_DID_DISMISS("onVideoFullscreenPlayerDidDismiss"),
EVENT_READY("onReadyForDisplay"),
EVENT_BUFFER("onVideoBuffer"),
EVENT_PLAYBACK_STATE_CHANGED("onVideoPlaybackStateChanged"),
EVENT_IDLE("onVideoIdle"),
EVENT_TIMED_METADATA("onTimedMetadata"),
EVENT_AUDIO_BECOMING_NOISY("onVideoAudioBecomingNoisy"),
EVENT_AUDIO_FOCUS_CHANGE("onAudioFocusChanged"),
EVENT_PLAYBACK_RATE_CHANGE("onPlaybackRateChange"),
EVENT_VOLUME_CHANGE("onVolumeChange"),
EVENT_AUDIO_TRACKS("onAudioTracks"),
EVENT_TEXT_TRACKS("onTextTracks"),
EVENT_TEXT_TRACK_DATA_CHANGED("onTextTrackDataChanged"),
EVENT_VIDEO_TRACKS("onVideoTracks"),
EVENT_ON_RECEIVE_AD_EVENT("onReceiveAdEvent"),
EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED("onPictureInPictureStatusChanged");
companion object {
fun toMap() =
mutableMapOf<String, Any>().apply {
EventTypes.values().toList().forEach { eventType ->
put("top${eventType.eventName.removePrefix("on")}", hashMapOf("registrationName" to eventType.eventName))
}
}
}
}
class VideoEventEmitter {
lateinit var onVideoLoadStart: () -> Unit
lateinit var onVideoLoad: (
duration: Long,
currentPosition: Long,
videoWidth: Int,
videoHeight: Int,
audioTracks: ArrayList<Track>,
textTracks: ArrayList<Track>,
videoTracks: ArrayList<VideoTrack>,
trackId: String?
) -> Unit
lateinit var onVideoError: (errorString: String, exception: Exception, errorCode: String) -> Unit
lateinit var onVideoProgress: (currentPosition: Long, bufferedDuration: Long, seekableDuration: Long, currentPlaybackTime: Double) -> Unit
lateinit var onVideoBandwidthUpdate: (bitRateEstimate: Long, height: Int, width: Int, trackId: String?) -> Unit
lateinit var onVideoPlaybackStateChanged: (isPlaying: Boolean, isSeeking: Boolean) -> Unit
lateinit var onVideoSeek: (currentPosition: Long, seekTime: Long) -> Unit
lateinit var onVideoEnd: () -> Unit
lateinit var onVideoFullscreenPlayerWillPresent: () -> Unit
lateinit var onVideoFullscreenPlayerDidPresent: () -> Unit
lateinit var onVideoFullscreenPlayerWillDismiss: () -> Unit
lateinit var onVideoFullscreenPlayerDidDismiss: () -> Unit
lateinit var onReadyForDisplay: () -> Unit
lateinit var onVideoBuffer: (isBuffering: Boolean) -> Unit
lateinit var onControlsVisibilityChange: (isVisible: Boolean) -> Unit
lateinit var onVideoIdle: () -> Unit
lateinit var onTimedMetadata: (metadataArrayList: ArrayList<TimedMetadata>) -> Unit
lateinit var onVideoAudioBecomingNoisy: () -> Unit
lateinit var onAudioFocusChanged: (hasFocus: Boolean) -> Unit
lateinit var onPlaybackRateChange: (rate: Float) -> Unit
lateinit var onVolumeChange: (volume: Float) -> Unit
lateinit var onAudioTracks: (audioTracks: ArrayList<Track>?) -> Unit
lateinit var onTextTracks: (textTracks: ArrayList<Track>?) -> Unit
lateinit var onVideoTracks: (videoTracks: ArrayList<VideoTrack>?) -> Unit
lateinit var onTextTrackDataChanged: (textTrackData: String) -> Unit
lateinit var onReceiveAdEvent: (adEvent: String, adData: Map<String?, String?>?) -> Unit
lateinit var onPictureInPictureStatusChanged: (isActive: Boolean) -> Unit
fun addEventEmitters(reactContext: ThemedReactContext, view: ReactExoplayerView) {
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
if (dispatcher != null) {
val event = EventBuilder(surfaceId, view.id, dispatcher)
onVideoLoadStart = {
event.dispatch(EventTypes.EVENT_LOAD_START)
}
onVideoLoad = { duration, currentPosition, videoWidth, videoHeight, audioTracks, textTracks, videoTracks, trackId ->
event.dispatch(EventTypes.EVENT_LOAD) {
putDouble("duration", duration / 1000.0)
putDouble("currentTime", currentPosition / 1000.0)
val naturalSize: WritableMap = aspectRatioToNaturalSize(videoWidth, videoHeight)
putMap("naturalSize", naturalSize)
trackId?.let { putString("trackId", it) }
putArray("videoTracks", videoTracksToArray(videoTracks))
putArray("audioTracks", audioTracksToArray(audioTracks))
putArray("textTracks", textTracksToArray(textTracks))
// TODO: Actually check if you can.
putBoolean("canPlayFastForward", true)
putBoolean("canPlaySlowForward", true)
putBoolean("canPlaySlowReverse", true)
putBoolean("canPlayReverse", true)
putBoolean("canPlayFastForward", true)
putBoolean("canStepBackward", true)
putBoolean("canStepForward", true)
}
}
onVideoError = { errorString, exception, errorCode ->
event.dispatch(EventTypes.EVENT_ERROR) {
putMap(
"error",
Arguments.createMap().apply {
// Prepare stack trace
val sw = StringWriter()
val pw = PrintWriter(sw)
exception.printStackTrace(pw)
val stackTrace = sw.toString()
putString("errorString", errorString)
putString("errorException", exception.toString())
putString("errorCode", errorCode)
putString("errorStackTrace", stackTrace)
}
)
}
}
onVideoProgress = { currentPosition, bufferedDuration, seekableDuration, currentPlaybackTime ->
event.dispatch(EventTypes.EVENT_PROGRESS) {
putDouble("currentTime", currentPosition / 1000.0)
putDouble("playableDuration", bufferedDuration / 1000.0)
putDouble("seekableDuration", seekableDuration / 1000.0)
putDouble("currentPlaybackTime", currentPlaybackTime)
}
}
onVideoBandwidthUpdate = { bitRateEstimate, height, width, trackId ->
event.dispatch(EventTypes.EVENT_BANDWIDTH) {
putDouble("bitrate", bitRateEstimate.toDouble())
if (width > 0) {
putInt("width", width)
}
if (height > 0) {
putInt("height", height)
}
trackId?.let { putString("trackId", it) }
}
}
onVideoPlaybackStateChanged = { isPlaying, isSeeking ->
event.dispatch(EventTypes.EVENT_PLAYBACK_STATE_CHANGED) {
putBoolean("isPlaying", isPlaying)
putBoolean("isSeeking", isSeeking)
}
}
onVideoSeek = { currentPosition, seekTime ->
event.dispatch(EventTypes.EVENT_SEEK) {
putDouble("currentTime", currentPosition / 1000.0)
putDouble("seekTime", seekTime / 1000.0)
}
}
onVideoEnd = {
event.dispatch(EventTypes.EVENT_END)
}
onVideoFullscreenPlayerWillPresent = {
event.dispatch(EventTypes.EVENT_FULLSCREEN_WILL_PRESENT)
}
onVideoFullscreenPlayerDidPresent = {
event.dispatch(EventTypes.EVENT_FULLSCREEN_DID_PRESENT)
}
onVideoFullscreenPlayerWillDismiss = {
event.dispatch(EventTypes.EVENT_FULLSCREEN_WILL_DISMISS)
}
onVideoFullscreenPlayerDidDismiss = {
event.dispatch(EventTypes.EVENT_FULLSCREEN_DID_DISMISS)
}
onReadyForDisplay = {
event.dispatch(EventTypes.EVENT_READY)
}
onVideoBuffer = { isBuffering ->
event.dispatch(EventTypes.EVENT_BUFFER) {
putBoolean("isBuffering", isBuffering)
}
}
onControlsVisibilityChange = { isVisible ->
event.dispatch(EventTypes.EVENT_CONTROLS_VISIBILITY_CHANGE) {
putBoolean("isVisible", isVisible)
}
}
onVideoIdle = {
event.dispatch(EventTypes.EVENT_IDLE)
}
onTimedMetadata = fn@{ metadataArrayList ->
if (metadataArrayList.size == 0) {
return@fn
}
event.dispatch(EventTypes.EVENT_TIMED_METADATA) {
putArray(
"metadata",
Arguments.createArray().apply {
metadataArrayList.forEachIndexed { _, metadata ->
pushMap(
Arguments.createMap().apply {
putString("identifier", metadata.identifier)
putString("value", metadata.value)
}
)
}
}
)
}
}
onVideoAudioBecomingNoisy = {
event.dispatch(EventTypes.EVENT_AUDIO_BECOMING_NOISY)
}
onAudioFocusChanged = { hasFocus ->
event.dispatch(EventTypes.EVENT_AUDIO_FOCUS_CHANGE) {
putBoolean("hasAudioFocus", hasFocus)
}
}
onPlaybackRateChange = { rate ->
event.dispatch(EventTypes.EVENT_PLAYBACK_RATE_CHANGE) {
putDouble("playbackRate", rate.toDouble())
}
}
onVolumeChange = { volume ->
event.dispatch(EventTypes.EVENT_VOLUME_CHANGE) {
putDouble("volume", volume.toDouble())
}
}
onAudioTracks = { audioTracks ->
event.dispatch(EventTypes.EVENT_AUDIO_TRACKS) {
putArray("audioTracks", audioTracksToArray(audioTracks))
}
}
onTextTracks = { textTracks ->
event.dispatch(EventTypes.EVENT_TEXT_TRACKS) {
putArray("textTracks", textTracksToArray(textTracks))
}
}
onVideoTracks = { videoTracks ->
event.dispatch(EventTypes.EVENT_VIDEO_TRACKS) {
putArray("videoTracks", videoTracksToArray(videoTracks))
}
}
onTextTrackDataChanged = { textTrackData ->
event.dispatch(EventTypes.EVENT_TEXT_TRACK_DATA_CHANGED) {
putString("subtitleTracks", textTrackData)
}
}
onReceiveAdEvent = { adEvent, adData ->
event.dispatch(EventTypes.EVENT_ON_RECEIVE_AD_EVENT) {
putString("event", adEvent)
putMap(
"data",
Arguments.createMap().apply {
adData?.let { data ->
for ((key, value) in data) {
putString(key!!, value)
}
}
}
)
}
}
onPictureInPictureStatusChanged = { isActive ->
event.dispatch(EventTypes.EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED) {
putBoolean("isActive", isActive)
}
}
}
}
private class VideoCustomEvent(surfaceId: Int, viewId: Int, private val event: EventTypes, private val paramsSetter: (WritableMap.() -> Unit)?) :
Event<VideoCustomEvent>(surfaceId, viewId) {
override fun getEventName(): String = "top${event.eventName.removePrefix("on")}"
override fun getEventData(): WritableMap? = Arguments.createMap().apply(paramsSetter ?: {})
}
private class EventBuilder(private val surfaceId: Int, private val viewId: Int, private val dispatcher: EventDispatcher) {
fun dispatch(event: EventTypes, paramsSetter: (WritableMap.() -> Unit)? = null) =
dispatcher.dispatchEvent(VideoCustomEvent(surfaceId, viewId, event, paramsSetter))
}
private fun audioTracksToArray(audioTracks: java.util.ArrayList<Track>?): WritableArray =
Arguments.createArray().apply {
audioTracks?.forEachIndexed { i, format ->
pushMap(
Arguments.createMap().apply {
putInt("index", i)
putString("title", format.title)
format.mimeType?.let { putString("type", it) }
format.language?.let { putString("language", it) }
if (format.bitrate > 0) putInt("bitrate", format.bitrate)
putBoolean("selected", format.isSelected)
}
)
}
}
private fun videoTracksToArray(videoTracks: java.util.ArrayList<VideoTrack>?): WritableArray =
Arguments.createArray().apply {
videoTracks?.forEachIndexed { _, vTrack ->
pushMap(
Arguments.createMap().apply {
putInt("width", vTrack.width)
putInt("height", vTrack.height)
putInt("bitrate", vTrack.bitrate)
putString("codecs", vTrack.codecs)
putString("trackId", vTrack.trackId)
putInt("index", vTrack.index)
putBoolean("selected", vTrack.isSelected)
putInt("rotation", vTrack.rotation)
}
)
}
}
private fun textTracksToArray(textTracks: ArrayList<Track>?): WritableArray =
Arguments.createArray().apply {
textTracks?.forEachIndexed { i, format ->
pushMap(
Arguments.createMap().apply {
putInt("index", i)
putString("title", format.title)
putString("type", format.mimeType)
putString("language", format.language)
putBoolean("selected", format.isSelected)
}
)
}
}
private fun aspectRatioToNaturalSize(videoWidth: Int, videoHeight: Int): WritableMap =
Arguments.createMap().apply {
if (videoWidth > 0) {
putInt("width", videoWidth)
}
if (videoHeight > 0) {
putInt("height", videoHeight)
}
val orientation = when {
videoWidth > videoHeight -> "landscape"
videoWidth < videoHeight -> "portrait"
else -> "square"
}
putString("orientation", orientation)
}
}

View File

@@ -1,96 +0,0 @@
package com.brentvatne.common.toolbox
import android.os.Build
import android.util.Log
import java.lang.Exception
/* log utils
* This class allow defining a log level for the package
* This is useful for debugging real time issue or tricky use cases
*/
object DebugLog {
// log level to display
private var level = Log.WARN
// enable thread display in logs
private var displayThread = true
// add a common prefix for easy filtering
private const val TAG_PREFIX = "RNV"
@JvmStatic
fun setConfig(_level: Int, _displayThread: Boolean) {
level = _level
displayThread = _displayThread
}
@JvmStatic
private fun getTag(tag: String): String = TAG_PREFIX + tag
@JvmStatic
private fun getMsg(msg: String): String =
if (displayThread) {
"[" + Thread.currentThread().name + "] " + msg
} else {
msg
}
@JvmStatic
fun v(tag: String, msg: String) {
if (level <= Log.VERBOSE) Log.v(getTag(tag), getMsg(msg))
}
@JvmStatic
fun d(tag: String, msg: String) {
if (level <= Log.DEBUG) Log.d(getTag(tag), getMsg(msg))
}
@JvmStatic
fun i(tag: String, msg: String) {
if (level <= Log.INFO) Log.i(getTag(tag), getMsg(msg))
}
@JvmStatic
fun w(tag: String, msg: String) {
if (level <= Log.WARN) Log.w(getTag(tag), getMsg(msg))
}
@JvmStatic
fun e(tag: String, msg: String) {
if (level <= Log.ERROR) Log.e(getTag(tag), getMsg(msg))
}
@JvmStatic
fun wtf(tag: String, msg: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
Log.wtf(getTag(tag), "--------------->" + getMsg(msg))
} else {
Log.e(getTag(tag), "--------------->" + getMsg(msg))
}
printCallStack()
}
@JvmStatic
fun printCallStack() {
if (level <= Log.VERBOSE) {
val e = Exception()
e.printStackTrace()
}
}
// Additionnal thread safety checkers
@JvmStatic
fun checkUIThread(tag: String, msg: String) {
if (Thread.currentThread().name != "main") {
wtf(tag, "------------------------>" + getMsg(msg))
}
}
@JvmStatic
fun checkNotUIThread(tag: String, msg: String) {
if (Thread.currentThread().name == "main") {
wtf(tag, "------------------------>" + getMsg(msg))
}
}
}

View File

@@ -1,141 +0,0 @@
package com.brentvatne.common.toolbox
import com.facebook.react.bridge.Dynamic
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
/*
* Toolbox to safe parsing of <Video props
* These are just safe accessors to ReadableMap
*/
object ReactBridgeUtils {
@JvmStatic
fun safeGetString(map: ReadableMap?, key: String?, fallback: String?): String? =
if (map != null && map.hasKey(key!!) && !map.isNull(key)) map.getString(key) else fallback
@JvmStatic
fun safeGetString(map: ReadableMap?, key: String?): String? = safeGetString(map, key, null)
@JvmStatic
fun safeGetDynamic(map: ReadableMap?, key: String?, fallback: Dynamic?): Dynamic? =
if (map != null && map.hasKey(key!!) && !map.isNull(key)) map.getDynamic(key) else fallback
@JvmStatic
fun safeGetDynamic(map: ReadableMap?, key: String?): Dynamic? = safeGetDynamic(map, key, null)
@JvmStatic
fun safeGetBool(map: ReadableMap?, key: String?, fallback: Boolean): Boolean =
if (map != null && map.hasKey(key!!) && !map.isNull(key)) map.getBoolean(key) else fallback
@JvmStatic
fun safeGetMap(map: ReadableMap?, key: String?): ReadableMap? = if (map != null && map.hasKey(key!!) && !map.isNull(key)) map.getMap(key) else null
@JvmStatic
fun safeGetArray(map: ReadableMap?, key: String?): ReadableArray? = if (map != null && map.hasKey(key!!) && !map.isNull(key)) map.getArray(key) else null
@JvmStatic
fun safeGetInt(map: ReadableMap?, key: String?, fallback: Int): Int =
if (map != null && map.hasKey(key!!) && !map.isNull(key)) map.getInt(key) else fallback
@JvmStatic
fun safeGetInt(map: ReadableMap?, key: String?): Int = safeGetInt(map, key, 0)
@JvmStatic
fun safeGetDouble(map: ReadableMap?, key: String?, fallback: Double): Double =
if (map != null && map.hasKey(key!!) && !map.isNull(key)) map.getDouble(key) else fallback
@JvmStatic
fun safeGetDouble(map: ReadableMap?, key: String?): Double = safeGetDouble(map, key, 0.0)
@JvmStatic fun safeGetFloat(map: ReadableMap?, key: String?, fallback: Float): Float =
if (map != null && map.hasKey(key!!) && !map.isNull(key)) map.getDouble(key).toFloat() else fallback
@JvmStatic fun safeGetFloat(map: ReadableMap?, key: String?): Float = safeGetFloat(map, key, 0.0f)
@JvmStatic fun safeParseInt(value: String?, default: Int): Int {
if (value == null) {
return default
}
return try {
value.toInt()
} catch (e: java.lang.Exception) {
default
}
}
/**
* toStringMap converts a [ReadableMap] into a HashMap.
*
* @param readableMap The ReadableMap to be conveted.
* @return A HashMap containing the data that was in the ReadableMap.
* @see 'Adapted from https://github.com/artemyarulin/react-native-eval/blob/master/android/src/main/java/com/evaluator/react/ConversionUtil.java'
*/
@JvmStatic
fun toStringMap(readableMap: ReadableMap?): Map<String, String?>? {
if (readableMap == null) return null
val iterator = readableMap.keySetIterator()
if (!iterator.hasNextKey()) return null
val result: MutableMap<String, String?> = HashMap()
while (iterator.hasNextKey()) {
val key = iterator.nextKey()
result[key] = readableMap.getString(key)
}
return result
}
/**
* toIntMap converts a [ReadableMap] into a HashMap.
*
* @param readableMap The ReadableMap to be conveted.
* @return A HashMap containing the data that was in the ReadableMap.
* @see 'Adapted from https://github.com/artemyarulin/react-native-eval/blob/master/android/src/main/java/com/evaluator/react/ConversionUtil.java'
*/
@JvmStatic
fun toIntMap(readableMap: ReadableMap?): Map<String, Int>? {
if (readableMap == null) return null
val iterator = readableMap.keySetIterator()
if (!iterator.hasNextKey()) return null
val result: MutableMap<String, Int> = HashMap()
while (iterator.hasNextKey()) {
val key = iterator.nextKey()
result[key] = readableMap.getInt(key)
}
return result
}
@JvmStatic
fun safeStringEquals(str1: String?, str2: String?): Boolean {
if (str1 == null && str2 == null) return true // both are null
return if (str1 == null || str2 == null) false else str1 == str2 // only 1 is null
}
@JvmStatic
fun safeStringArrayEquals(str1: Array<String>?, str2: Array<String>?): Boolean {
if (str1 == null && str2 == null) return true // both are null
if (str1 == null || str2 == null) return false // only 1 is null
if (str1.size != str2.size) return false // only 1 is null
for (i in str1.indices) {
if (str1[i] == str2[i]) {
// standard check
return false
}
}
return true
}
@JvmStatic
fun safeStringMapEquals(first: Map<String?, String?>?, second: Map<String?, String?>?): Boolean {
if (first == null && second == null) return true // both are null
if (first == null || second == null) return false // only 1 is null
if (first.size != second.size) {
return false
}
for (key in first.keys) {
if (!safeStringEquals(first[key], second[key])) {
return false
}
}
return true
}
}

View File

@@ -1,106 +0,0 @@
package com.brentvatne.exoplayer
import android.content.Context
import android.widget.FrameLayout
import androidx.media3.common.Format
import com.brentvatne.common.api.ResizeMode
import kotlin.math.abs
/**
* A {@link FrameLayout} that resizes itself to match a specified aspect ratio.
*/
class AspectRatioFrameLayout(context: Context) : FrameLayout(context) {
/**
* The {@link FrameLayout} will not resize itself if the fractional difference between its natural
* aspect ratio and the requested aspect ratio falls below this threshold.
* <p>
* This tolerance allows the view to occupy the whole of the screen when the requested aspect
* ratio is very close, but not exactly equal to, the aspect ratio of the screen. This may reduce
* the number of view layers that need to be composited by the underlying system, which can help
* to reduce power consumption.
*/
companion object {
private const val MAX_ASPECT_RATIO_DEFORMATION_FRACTION = 0.01f
}
var videoAspectRatio: Float = 0f
set(value) {
if (value != field) {
field = value
requestLayout()
}
}
var resizeMode: Int = ResizeMode.RESIZE_MODE_FIT
set(value) {
if (value != field) {
field = value
requestLayout()
}
}
fun invalidateAspectRatio() {
videoAspectRatio = 0f
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (videoAspectRatio == 0f) {
// Aspect ratio not set.
return
}
val measuredWidth: Int = measuredWidth
val measuredHeight: Int = measuredHeight
var width: Int = measuredWidth
var height: Int = measuredHeight
val viewAspectRatio: Float = measuredWidth.toFloat() / measuredHeight
val aspectDeformation: Float = videoAspectRatio / viewAspectRatio - 1
if (abs(aspectDeformation) <= MAX_ASPECT_RATIO_DEFORMATION_FRACTION) {
// We're within the allowed tolerance.
return
}
when (resizeMode) {
ResizeMode.RESIZE_MODE_FIXED_WIDTH -> height = (measuredWidth / videoAspectRatio).toInt()
ResizeMode.RESIZE_MODE_FIXED_HEIGHT -> width = ((measuredHeight * videoAspectRatio).toInt())
ResizeMode.RESIZE_MODE_FILL -> {
// Do nothing width and height is the same as the view
}
ResizeMode.RESIZE_MODE_CENTER_CROP -> {
width = (measuredHeight * videoAspectRatio).toInt()
// Scale video if it doesn't fill the measuredWidth
if (width < measuredWidth) {
val scaleFactor: Float = measuredWidth.toFloat() / width
width = (scaleFactor * width).toInt()
height = (scaleFactor * height).toInt()
}
}
else -> {
if (aspectDeformation > 0) {
height = (measuredWidth / videoAspectRatio).toInt()
} else {
width = (measuredHeight * videoAspectRatio).toInt()
}
}
}
super.onMeasure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
)
}
fun updateAspectRatio(format: Format) {
// There are weird cases when video height and width did not change with rotation so we need change aspect ration to fix it
when (format.rotationDegrees) {
90, 270 -> videoAspectRatio = if (format.width == 0) 1f else (format.height * format.pixelWidthHeightRatio) / format.width
else -> videoAspectRatio = if (format.height == 0) 1f else (format.width * format.pixelWidthHeightRatio) / format.height
}
}
}

View File

@@ -1,25 +0,0 @@
package com.brentvatne.exoplayer
import android.annotation.SuppressLint
import androidx.media3.common.C
@SuppressLint("InlinedApi")
enum class AudioOutput(private val outputName: String, @C.StreamType val streamType: Int) {
SPEAKER("speaker", C.STREAM_TYPE_MUSIC),
EARPIECE("earpiece", C.STREAM_TYPE_VOICE_CALL);
companion object {
@JvmStatic
fun get(name: String): AudioOutput {
for (entry in values()) {
if (entry.outputName.equals(name, ignoreCase = true)) {
return entry
}
}
return SPEAKER
}
}
override fun toString(): String = "${javaClass.simpleName}($outputName, $streamType)"
}

View File

@@ -1,54 +0,0 @@
package com.brentvatne.exoplayer
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.upstream.CmcdConfiguration
import com.brentvatne.common.api.CMCDProps
import com.brentvatne.common.toolbox.DebugLog
import com.google.common.collect.ImmutableListMultimap
class CMCDConfig(private val props: CMCDProps) {
fun toCmcdConfigurationFactory(): CmcdConfiguration.Factory = CmcdConfiguration.Factory(::createCmcdConfiguration)
private fun createCmcdConfiguration(mediaItem: MediaItem): CmcdConfiguration =
CmcdConfiguration(
java.util.UUID.randomUUID().toString(),
mediaItem.mediaId,
object : CmcdConfiguration.RequestConfig {
override fun getCustomData(): ImmutableListMultimap<String, String> = buildCustomData()
},
intToCmcdMode(props.mode)
)
private fun intToCmcdMode(mode: Int): Int =
when (mode) {
0 -> CmcdConfiguration.MODE_REQUEST_HEADER
1 -> CmcdConfiguration.MODE_QUERY_PARAMETER
else -> {
DebugLog.e("CMCDConfig", "Unsupported mode: $mode, fallback on MODE_REQUEST_HEADER")
CmcdConfiguration.MODE_REQUEST_HEADER
}
}
private fun buildCustomData(): ImmutableListMultimap<String, String> =
ImmutableListMultimap.builder<String, String>().apply {
addFormattedData(this, CmcdConfiguration.KEY_CMCD_OBJECT, props.cmcdObject)
addFormattedData(this, CmcdConfiguration.KEY_CMCD_REQUEST, props.cmcdRequest)
addFormattedData(this, CmcdConfiguration.KEY_CMCD_SESSION, props.cmcdSession)
addFormattedData(this, CmcdConfiguration.KEY_CMCD_STATUS, props.cmcdStatus)
}.build()
private fun addFormattedData(builder: ImmutableListMultimap.Builder<String, String>, key: String, dataList: List<Pair<String, Any>>) {
dataList.forEach { (dataKey, dataValue) ->
builder.put(key, formatKeyValue(dataKey, dataValue))
}
}
private fun formatKeyValue(key: String, value: Any): String =
when (value) {
is String -> "$key=\"$value\""
is Number -> "$key=$value"
else -> throw IllegalArgumentException("Unsupported value type: ${value::class.java}")
}
}

View File

@@ -1,56 +0,0 @@
package com.brentvatne.exoplayer
import androidx.media3.common.MediaItem.LiveConfiguration
import androidx.media3.common.MediaMetadata
import com.brentvatne.common.api.BufferConfig
import com.brentvatne.common.api.BufferConfig.Live
import com.brentvatne.common.api.Source
/**
* Helper functions to create exoplayer configuration
*/
object ConfigurationUtils {
/**
* Create a media3.LiveConfiguration.Builder from parsed BufferConfig
*/
@JvmStatic
fun getLiveConfiguration(bufferConfig: BufferConfig): LiveConfiguration.Builder {
val liveConfiguration = LiveConfiguration.Builder()
val live: Live = bufferConfig.live
if (bufferConfig.live.maxOffsetMs >= 0) {
liveConfiguration.setMaxOffsetMs(live.maxOffsetMs)
}
if (bufferConfig.live.maxPlaybackSpeed >= 0) {
liveConfiguration.setMaxPlaybackSpeed(live.maxPlaybackSpeed)
}
if (bufferConfig.live.targetOffsetMs >= 0) {
liveConfiguration.setTargetOffsetMs(live.targetOffsetMs)
}
if (bufferConfig.live.minOffsetMs >= 0) {
liveConfiguration.setMinOffsetMs(live.minOffsetMs)
}
if (bufferConfig.live.minPlaybackSpeed >= 0) {
liveConfiguration.setMinPlaybackSpeed(live.minPlaybackSpeed)
}
return liveConfiguration
}
/**
* Generate exoplayer MediaMetadata from source.Metadata
*/
@JvmStatic
fun buildCustomMetadata(metadata: Source.Metadata?): MediaMetadata? {
var customMetadata: MediaMetadata? = null
if (metadata != null) {
customMetadata = MediaMetadata.Builder()
.setTitle(metadata.title)
.setSubtitle(metadata.subtitle)
.setDescription(metadata.description)
.setArtist(metadata.artist)
.setArtworkUri(metadata.imageUri)
.build()
}
return customMetadata
}
}

View File

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

View File

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

View File

@@ -1,88 +0,0 @@
package com.brentvatne.exoplayer
import android.net.Uri
import androidx.media3.common.util.Util
import androidx.media3.datasource.AssetDataSource
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DataSpec
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter
import com.facebook.react.bridge.ReactContext
import com.facebook.react.modules.network.CookieJarContainer
import com.facebook.react.modules.network.ForwardingCookieHandler
import com.facebook.react.modules.network.OkHttpClientProvider
import okhttp3.Call
import okhttp3.JavaNetCookieJar
object DataSourceUtil {
private var defaultDataSourceFactory: DataSource.Factory? = null
private var defaultHttpDataSourceFactory: HttpDataSource.Factory? = null
private var userAgent: String? = null
private fun getUserAgent(context: ReactContext): String {
if (userAgent == null) {
userAgent = Util.getUserAgent(context, context.packageName)
}
return userAgent as String
}
@JvmStatic
fun getDefaultDataSourceFactory(context: ReactContext, bandwidthMeter: DefaultBandwidthMeter?, requestHeaders: Map<String, String>?): DataSource.Factory {
if (defaultDataSourceFactory == null || !requestHeaders.isNullOrEmpty()) {
defaultDataSourceFactory = buildDataSourceFactory(context, bandwidthMeter, requestHeaders)
}
return defaultDataSourceFactory as DataSource.Factory
}
@JvmStatic
fun getDefaultHttpDataSourceFactory(
context: ReactContext,
bandwidthMeter: DefaultBandwidthMeter?,
requestHeaders: Map<String, String>?
): HttpDataSource.Factory {
if (defaultHttpDataSourceFactory == null || !requestHeaders.isNullOrEmpty()) {
defaultHttpDataSourceFactory = buildHttpDataSourceFactory(context, bandwidthMeter, requestHeaders)
}
return defaultHttpDataSourceFactory as HttpDataSource.Factory
}
private fun buildDataSourceFactory(
context: ReactContext,
bandwidthMeter: DefaultBandwidthMeter?,
requestHeaders: Map<String, String>?
): DataSource.Factory = DefaultDataSource.Factory(context, buildHttpDataSourceFactory(context, bandwidthMeter, requestHeaders))
private fun buildHttpDataSourceFactory(
context: ReactContext,
bandwidthMeter: DefaultBandwidthMeter?,
requestHeaders: Map<String, String>?
): HttpDataSource.Factory {
val client = OkHttpClientProvider.getOkHttpClient()
val container = client.cookieJar as CookieJarContainer
val handler = ForwardingCookieHandler(context)
container.setCookieJar(JavaNetCookieJar(handler))
val okHttpDataSourceFactory = OkHttpDataSource.Factory(client as Call.Factory)
.setTransferListener(bandwidthMeter)
if (requestHeaders != null) {
okHttpDataSourceFactory.setDefaultRequestProperties(requestHeaders)
if (!requestHeaders.containsKey("User-Agent")) {
okHttpDataSourceFactory.setUserAgent(getUserAgent(context))
}
} else {
okHttpDataSourceFactory.setUserAgent(getUserAgent(context))
}
return okHttpDataSourceFactory
}
@JvmStatic
fun buildAssetDataSourceFactory(context: ReactContext?, srcUri: Uri?): DataSource.Factory {
val dataSpec = DataSpec(srcUri!!)
val rawResourceDataSource = AssetDataSource(context!!)
rawResourceDataSource.open(dataSpec)
return DataSource.Factory { rawResourceDataSource }
}
}

View File

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

View File

@@ -1,337 +0,0 @@
package com.brentvatne.exoplayer
import android.content.Context
import android.util.Log
import android.util.TypedValue
import android.view.Gravity
import android.view.SurfaceView
import android.view.TextureView
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.content.ContextCompat
import androidx.media3.common.AdViewProvider
import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.common.Tracks
import androidx.media3.common.VideoSize
import androidx.media3.common.text.Cue
import androidx.media3.common.util.Assertions
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.SubtitleView
import com.brentvatne.common.api.ResizeMode
import com.brentvatne.common.api.SubtitleStyle
import com.brentvatne.common.api.ViewType
import com.brentvatne.common.toolbox.DebugLog
@UnstableApi
class ExoPlayerView(private val context: Context) :
FrameLayout(context, null, 0),
AdViewProvider {
var surfaceView: View? = null
private set
private var shutterView: View
private var subtitleLayout: SubtitleView
private var layout: AspectRatioFrameLayout
private var componentListener: ComponentListener
private var player: ExoPlayer? = null
private var layoutParams: ViewGroup.LayoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
private var adOverlayFrameLayout: FrameLayout? = null
val isPlaying: Boolean
get() = player != null && player?.isPlaying == true
@ViewType.ViewType
private var viewType = ViewType.VIEW_TYPE_SURFACE
private var hideShutterView = false
private var localStyle = SubtitleStyle()
init {
componentListener = ComponentListener()
val aspectRatioParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT
)
aspectRatioParams.gravity = Gravity.CENTER
layout = AspectRatioFrameLayout(context)
layout.layoutParams = aspectRatioParams
shutterView = View(context)
shutterView.layoutParams = layoutParams
shutterView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.black))
subtitleLayout = SubtitleView(context)
subtitleLayout.layoutParams = layoutParams
subtitleLayout.setUserDefaultStyle()
subtitleLayout.setUserDefaultTextSize()
updateSurfaceView(viewType)
layout.addView(shutterView, 1, layoutParams)
if (localStyle.subtitlesFollowVideo) {
layout.addView(subtitleLayout, layoutParams)
}
addViewInLayout(layout, 0, aspectRatioParams)
if (!localStyle.subtitlesFollowVideo) {
addViewInLayout(subtitleLayout, 1, layoutParams)
}
}
private fun clearVideoView() {
when (val view = surfaceView) {
is TextureView -> player?.clearVideoTextureView(view)
is SurfaceView -> player?.clearVideoSurfaceView(view)
else -> {
Log.w(
"clearVideoView",
"Unexpected surfaceView type: ${surfaceView?.javaClass?.name}"
)
}
}
}
private fun setVideoView() {
when (val view = surfaceView) {
is TextureView -> player?.setVideoTextureView(view)
is SurfaceView -> player?.setVideoSurfaceView(view)
else -> {
Log.w(
"setVideoView",
"Unexpected surfaceView type: ${surfaceView?.javaClass?.name}"
)
}
}
}
fun setSubtitleStyle(style: SubtitleStyle) {
// ensure we reset subtitle style before reapplying it
subtitleLayout.setUserDefaultStyle()
subtitleLayout.setUserDefaultTextSize()
if (style.fontSize > 0) {
subtitleLayout.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, style.fontSize.toFloat())
}
subtitleLayout.setPadding(
style.paddingLeft,
style.paddingTop,
style.paddingTop,
style.paddingBottom
)
if (style.opacity != 0.0f) {
subtitleLayout.alpha = style.opacity
subtitleLayout.visibility = View.VISIBLE
} else {
subtitleLayout.visibility = View.GONE
}
if (localStyle.subtitlesFollowVideo != style.subtitlesFollowVideo) {
// No need to manipulate layout if value didn't change
if (style.subtitlesFollowVideo) {
removeViewInLayout(subtitleLayout)
layout.addView(subtitleLayout, layoutParams)
} else {
layout.removeViewInLayout(subtitleLayout)
addViewInLayout(subtitleLayout, 1, layoutParams, false)
}
requestLayout()
}
localStyle = style
}
fun setShutterColor(color: Int) {
shutterView.setBackgroundColor(color)
}
fun updateSurfaceView(@ViewType.ViewType viewType: Int) {
this.viewType = viewType
var viewNeedRefresh = false
when (viewType) {
ViewType.VIEW_TYPE_SURFACE, ViewType.VIEW_TYPE_SURFACE_SECURE -> {
if (surfaceView !is SurfaceView) {
surfaceView = SurfaceView(context)
viewNeedRefresh = true
}
(surfaceView as SurfaceView).setSecure(viewType == ViewType.VIEW_TYPE_SURFACE_SECURE)
}
ViewType.VIEW_TYPE_TEXTURE -> {
if (surfaceView !is TextureView) {
surfaceView = TextureView(context)
viewNeedRefresh = true
}
// Support opacity properly:
(surfaceView as TextureView).isOpaque = false
}
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()
}
}
}
var adsShown = false
fun showAds() {
if (!adsShown) {
adOverlayFrameLayout = FrameLayout(context)
layout.addView(adOverlayFrameLayout, layoutParams)
adsShown = true
}
}
fun hideAds() {
if (adsShown) {
layout.removeView(adOverlayFrameLayout)
adOverlayFrameLayout = null
adsShown = false
}
}
fun updateShutterViewVisibility() {
shutterView.visibility = if (this.hideShutterView) {
View.INVISIBLE
} else {
View.VISIBLE
}
}
override fun requestLayout() {
super.requestLayout()
post(measureAndLayout)
}
// AdsLoader.AdViewProvider implementation.
override fun getAdViewGroup(): ViewGroup =
Assertions.checkNotNull(
adOverlayFrameLayout,
"exo_ad_overlay must be present for ad playback"
)
/**
* Set the {@link ExoPlayer} to use. The {@link ExoPlayer#addListener} method of the
* player will be called and previous
* assignments are overridden.
*
* @param player The {@link ExoPlayer} to use.
*/
fun setPlayer(player: ExoPlayer?) {
if (this.player == player) {
return
}
if (this.player != null) {
this.player!!.removeListener(componentListener)
clearVideoView()
}
this.player = player
updateShutterViewVisibility()
if (player != null) {
setVideoView()
player.addListener(componentListener)
}
}
/**
* Sets the resize mode which can be of value {@link ResizeMode.Mode}
*
* @param resizeMode The resize mode.
*/
fun setResizeMode(@ResizeMode.Mode resizeMode: Int) {
if (layout.resizeMode != resizeMode) {
layout.resizeMode = resizeMode
post(measureAndLayout)
}
}
fun setHideShutterView(hideShutterView: Boolean) {
this.hideShutterView = hideShutterView
updateShutterViewVisibility()
}
private val measureAndLayout: Runnable = Runnable {
measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
)
layout(left, top, right, bottom)
}
private fun updateForCurrentTrackSelections(tracks: Tracks?) {
if (tracks == null) {
return
}
val groups = tracks.groups
for (group in groups) {
if (group.type == C.TRACK_TYPE_VIDEO && group.length > 0) {
// get the first track of the group to identify aspect ratio
val format = group.getTrackFormat(0)
if (format.width > 0 || format.height > 0) {
layout.updateAspectRatio(format)
}
return
}
}
// no video tracks, in that case refresh shutterView visibility
updateShutterViewVisibility()
}
fun invalidateAspectRatio() {
// Resetting aspect ratio will force layout refresh on next video size changed
layout.invalidateAspectRatio()
}
private inner class ComponentListener : Player.Listener {
override fun onCues(cues: List<Cue>) {
subtitleLayout.setCues(cues)
}
override fun onVideoSizeChanged(videoSize: VideoSize) {
if (videoSize.height == 0 || videoSize.width == 0) {
// When changing video track we receive an ghost state with height / width = 0
// No need to resize the view in that case
return
}
// Here we use updateForCurrentTrackSelections to have a consistent behavior.
// according to: https://github.com/androidx/media/issues/1207
// sometimes media3 send bad Video size information
player?.let {
updateForCurrentTrackSelections(it.currentTracks)
}
}
override fun onRenderedFirstFrame() {
shutterView.visibility = INVISIBLE
}
override fun onTracksChanged(tracks: Tracks) {
updateForCurrentTrackSelections(tracks)
}
}
companion object {
private const val TAG = "ExoPlayerView"
}
}

View File

@@ -1,238 +0,0 @@
package com.brentvatne.exoplayer
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.activity.OnBackPressedCallback
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.media3.ui.LegacyPlayerControlView
import com.brentvatne.common.api.ControlsConfig
import com.brentvatne.common.toolbox.DebugLog
import java.lang.ref.WeakReference
@SuppressLint("PrivateResource")
class FullScreenPlayerView(
context: Context,
private val exoPlayerView: ExoPlayerView,
private val reactExoplayerView: ReactExoplayerView,
private val playerControlView: LegacyPlayerControlView?,
private val onBackPressedCallback: OnBackPressedCallback,
private val controlsConfig: ControlsConfig
) : Dialog(context, android.R.style.Theme_Black_NoTitleBar) {
private var parent: ViewGroup? = null
private val containerView = FrameLayout(context)
private val mKeepScreenOnHandler = Handler(Looper.getMainLooper())
private val mKeepScreenOnUpdater = KeepScreenOnUpdater(this)
// As this view is fullscreen we need to save initial state and restore it afterward
// Following variables save UI state when open the view
// restoreUIState, will reapply these values
private var initialSystemBarsBehavior: Int? = null
private var initialNavigationBarIsVisible: Boolean? = null
private var initialNotificationBarIsVisible: Boolean? = null
private class KeepScreenOnUpdater(fullScreenPlayerView: FullScreenPlayerView) : Runnable {
private val mFullscreenPlayer = WeakReference(fullScreenPlayerView)
override fun run() {
try {
val fullscreenVideoPlayer = mFullscreenPlayer.get()
if (fullscreenVideoPlayer != null) {
val window = fullscreenVideoPlayer.window
if (window != null) {
val isPlaying = fullscreenVideoPlayer.exoPlayerView.isPlaying
if (isPlaying) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
fullscreenVideoPlayer.mKeepScreenOnHandler.postDelayed(this, UPDATE_KEEP_SCREEN_ON_FLAG_MS)
}
} catch (ex: Exception) {
DebugLog.e("ExoPlayer Exception", "Failed to flag FLAG_KEEP_SCREEN_ON on fullscreen.")
DebugLog.e("ExoPlayer Exception", ex.toString())
}
}
companion object {
private const val UPDATE_KEEP_SCREEN_ON_FLAG_MS = 200L
}
}
init {
setContentView(containerView, generateDefaultLayoutParams())
window?.let {
val inset = WindowInsetsControllerCompat(it, it.decorView)
initialSystemBarsBehavior = inset.systemBarsBehavior
initialNavigationBarIsVisible = ViewCompat.getRootWindowInsets(it.decorView)
?.isVisible(WindowInsetsCompat.Type.navigationBars()) == true
initialNotificationBarIsVisible = ViewCompat.getRootWindowInsets(it.decorView)
?.isVisible(WindowInsetsCompat.Type.statusBars()) == true
}
}
override fun onStart() {
super.onStart()
parent = exoPlayerView.parent as ViewGroup?
parent?.removeView(exoPlayerView)
containerView.addView(exoPlayerView, generateDefaultLayoutParams())
playerControlView?.let {
updateFullscreenButton(playerControlView, true)
parent?.removeView(it)
containerView.addView(it, generateDefaultLayoutParams())
}
updateNavigationBarVisibility()
}
override fun onStop() {
super.onStop()
mKeepScreenOnHandler.removeCallbacks(mKeepScreenOnUpdater)
containerView.removeView(exoPlayerView)
parent?.addView(exoPlayerView, generateDefaultLayoutParams())
playerControlView?.let {
updateFullscreenButton(playerControlView, false)
containerView.removeView(it)
parent?.addView(it, generateDefaultLayoutParams())
}
parent?.requestLayout()
parent = null
onBackPressedCallback.handleOnBackPressed()
restoreSystemUI()
}
// restore system UI state
private fun restoreSystemUI() {
window?.let {
updateNavigationBarVisibility(
it,
initialNavigationBarIsVisible,
initialNotificationBarIsVisible,
initialSystemBarsBehavior
)
}
}
fun hideWithoutPlayer() {
for (i in 0 until containerView.childCount) {
if (containerView.getChildAt(i) !== exoPlayerView) {
containerView.getChildAt(i).visibility = View.GONE
}
}
}
private fun getFullscreenIconResource(isFullscreen: Boolean): Int =
if (isFullscreen) {
androidx.media3.ui.R.drawable.exo_icon_fullscreen_exit
} else {
androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter
}
private fun updateFullscreenButton(playerControlView: LegacyPlayerControlView, isFullscreen: Boolean) {
val imageButton = playerControlView.findViewById<ImageButton?>(com.brentvatne.react.R.id.exo_fullscreen)
imageButton?.let {
val imgResource = getFullscreenIconResource(isFullscreen)
val desc = if (isFullscreen) {
context.getString(androidx.media3.ui.R.string.exo_controls_fullscreen_exit_description)
} else {
context.getString(androidx.media3.ui.R.string.exo_controls_fullscreen_enter_description)
}
imageButton.setImageResource(imgResource)
imageButton.contentDescription = desc
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (reactExoplayerView.preventsDisplaySleepDuringVideoPlayback) {
mKeepScreenOnHandler.post(mKeepScreenOnUpdater)
}
}
private fun generateDefaultLayoutParams(): FrameLayout.LayoutParams {
val layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
layoutParams.setMargins(0, 0, 0, 0)
return layoutParams
}
private fun updateBarVisibility(
inset: WindowInsetsControllerCompat,
type: Int,
shouldHide: Boolean?,
initialVisibility: Boolean?,
systemBarsBehavior: Int? = null
) {
shouldHide?.takeIf { it != initialVisibility }?.let {
if (it) {
inset.hide(type)
systemBarsBehavior?.let { behavior -> inset.systemBarsBehavior = behavior }
} else {
inset.show(type)
}
}
}
// Move the UI to fullscreen.
// if you change this code, remember to check that the UI is well restored in restoreUIState
private fun updateNavigationBarVisibility(
window: Window,
hideNavigationBarOnFullScreenMode: Boolean?,
hideNotificationBarOnFullScreenMode: Boolean?,
systemBarsBehavior: Int?
) {
// Configure the behavior of the hidden system bars.
val inset = WindowInsetsControllerCompat(window, window.decorView)
// Update navigation bar visibility and apply systemBarsBehavior if hiding
updateBarVisibility(
inset,
WindowInsetsCompat.Type.navigationBars(),
hideNavigationBarOnFullScreenMode,
initialNavigationBarIsVisible,
systemBarsBehavior
)
// Update notification bar visibility (no need for systemBarsBehavior here)
updateBarVisibility(
inset,
WindowInsetsCompat.Type.statusBars(),
hideNotificationBarOnFullScreenMode,
initialNotificationBarIsVisible
)
}
private fun updateNavigationBarVisibility() {
window?.let {
updateNavigationBarVisibility(
it,
controlsConfig.hideNavigationBarOnFullScreenMode,
controlsConfig.hideNotificationBarOnFullScreenMode,
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
)
}
if (controlsConfig.hideNotificationBarOnFullScreenMode) {
val liveContainer = playerControlView?.findViewById<LinearLayout?>(com.brentvatne.react.R.id.exo_live_container)
liveContainer?.let {
val layoutParams = it.layoutParams as LinearLayout.LayoutParams
layoutParams.topMargin = 40
it.layoutParams = layoutParams
}
}
}
}

View File

@@ -1,208 +0,0 @@
package com.brentvatne.exoplayer
import android.annotation.SuppressLint
import android.app.AppOpsManager
import android.app.PictureInPictureParams
import android.app.RemoteAction
import android.content.Context
import android.content.ContextWrapper
import android.content.pm.PackageManager
import android.graphics.Rect
import android.graphics.drawable.Icon
import android.os.Build
import android.os.Process
import android.util.Rational
import androidx.activity.ComponentActivity
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.annotation.RequiresApi
import androidx.core.app.AppOpsManagerCompat
import androidx.core.app.PictureInPictureModeChangedInfo
import androidx.core.util.Consumer
import androidx.lifecycle.Lifecycle
import androidx.media3.exoplayer.ExoPlayer
import com.brentvatne.common.toolbox.DebugLog
import com.brentvatne.receiver.PictureInPictureReceiver
import com.facebook.react.uimanager.ThemedReactContext
internal fun Context.findActivity(): ComponentActivity {
var context = this
while (context is ContextWrapper) {
if (context is ComponentActivity) return context
context = context.baseContext
}
throw IllegalStateException("Picture in picture should be called in the context of an Activity")
}
object PictureInPictureUtil {
private const val FLAG_SUPPORTS_PICTURE_IN_PICTURE = 0x400000
private const val TAG = "PictureInPictureUtil"
@JvmStatic
fun addLifecycleEventListener(context: ThemedReactContext, view: ReactExoplayerView): Runnable {
val activity = context.findActivity()
val onPictureInPictureModeChanged = Consumer<PictureInPictureModeChangedInfo> { info ->
view.setIsInPictureInPicture(info.isInPictureInPictureMode)
if (!info.isInPictureInPictureMode && activity.lifecycle.currentState == Lifecycle.State.CREATED) {
// when user click close button of PIP
if (!view.playInBackground) view.setPausedModifier(true)
}
}
val onUserLeaveHintCallback = Runnable {
if (view.enterPictureInPictureOnLeave) {
view.enterPictureInPictureMode()
}
}
activity.addOnPictureInPictureModeChangedListener(onPictureInPictureModeChanged)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
activity.addOnUserLeaveHintListener(onUserLeaveHintCallback)
}
// @TODO convert to lambda when ReactExoplayerView migrated
return Runnable {
with(activity) {
removeOnPictureInPictureModeChangedListener(onPictureInPictureModeChanged)
removeOnUserLeaveHintListener(onUserLeaveHintCallback)
}
}
}
@JvmStatic
fun enterPictureInPictureMode(context: ThemedReactContext, pictureInPictureParams: PictureInPictureParams?) {
if (!isSupportPictureInPicture(context)) return
if (isSupportPictureInPictureAction() && pictureInPictureParams != null) {
try {
context.findActivity().enterPictureInPictureMode(pictureInPictureParams)
} catch (e: IllegalStateException) {
DebugLog.e(TAG, e.toString())
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
@Suppress("DEPRECATION")
context.findActivity().enterPictureInPictureMode()
} catch (e: IllegalStateException) {
DebugLog.e(TAG, e.toString())
}
}
}
@JvmStatic
fun applyPlayingStatus(
context: ThemedReactContext,
pipParamsBuilder: PictureInPictureParams.Builder?,
receiver: PictureInPictureReceiver,
isPaused: Boolean
) {
if (pipParamsBuilder == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val actions = getPictureInPictureActions(context, isPaused, receiver)
pipParamsBuilder.setActions(actions)
updatePictureInPictureActions(context, pipParamsBuilder.build())
}
@JvmStatic
fun applyAutoEnterEnabled(context: ThemedReactContext, pipParamsBuilder: PictureInPictureParams.Builder?, autoEnterEnabled: Boolean) {
if (pipParamsBuilder == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
pipParamsBuilder.setAutoEnterEnabled(autoEnterEnabled)
updatePictureInPictureActions(context, pipParamsBuilder.build())
}
@JvmStatic
fun applySourceRectHint(context: ThemedReactContext, pipParamsBuilder: PictureInPictureParams.Builder?, playerView: ExoPlayerView) {
if (pipParamsBuilder == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
pipParamsBuilder.setSourceRectHint(calcRectHint(playerView))
updatePictureInPictureActions(context, pipParamsBuilder.build())
}
private fun updatePictureInPictureActions(context: ThemedReactContext, pipParams: PictureInPictureParams) {
if (!isSupportPictureInPictureAction()) return
if (!isSupportPictureInPicture(context)) return
try {
context.findActivity().setPictureInPictureParams(pipParams)
} catch (e: IllegalStateException) {
DebugLog.e(TAG, e.toString())
}
}
@JvmStatic
@RequiresApi(Build.VERSION_CODES.O)
fun getPictureInPictureActions(context: ThemedReactContext, isPaused: Boolean, receiver: PictureInPictureReceiver): ArrayList<RemoteAction> {
val intent = receiver.getPipActionIntent(isPaused)
val resource =
if (isPaused) androidx.media3.ui.R.drawable.exo_icon_play else androidx.media3.ui.R.drawable.exo_icon_pause
val icon = Icon.createWithResource(context, resource)
val title = if (isPaused) "play" else "pause"
return arrayListOf(RemoteAction(icon, title, title, intent))
}
@JvmStatic
@RequiresApi(Build.VERSION_CODES.O)
private fun calcRectHint(playerView: ExoPlayerView): Rect {
val hint = Rect()
playerView.surfaceView?.getGlobalVisibleRect(hint)
val location = IntArray(2)
playerView.surfaceView?.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 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
val isActivitySupportPip = activityInfo.flags and FLAG_SUPPORTS_PICTURE_IN_PICTURE != 0
// PIP might be disabled on devices that have low RAM.
val isPipAvailable = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
return isActivitySupportPip && isPipAvailable
}
private fun checkIsUserAllowPIP(context: ThemedReactContext): Boolean {
val activity = context.currentActivity ?: return false
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@SuppressLint("InlinedApi")
val result = AppOpsManagerCompat.noteOpNoThrow(
activity,
AppOpsManager.OPSTR_PICTURE_IN_PICTURE,
Process.myUid(),
activity.packageName
)
AppOpsManager.MODE_ALLOWED == result
} else {
Build.VERSION.SDK_INT < Build.VERSION_CODES.O && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
}
}
}

View File

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

View File

@@ -1,12 +0,0 @@
package com.brentvatne.exoplayer
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
interface ReactExoplayerConfig {
fun buildLoadErrorHandlingPolicy(minLoadRetryCount: Int): LoadErrorHandlingPolicy
var disableDisconnectError: Boolean
val bandwidthMeter: DefaultBandwidthMeter
var initialBitrate: Long?
fun setInitialBitrate(bitrate: Long)
}

View File

@@ -1,27 +0,0 @@
package com.brentvatne.exoplayer
import androidx.media3.common.C
import androidx.media3.datasource.HttpDataSource.HttpDataSourceException
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy.LoadErrorInfo
import kotlin.math.min
class ReactExoplayerLoadErrorHandlingPolicy(private val minLoadRetryCount: Int) : DefaultLoadErrorHandlingPolicy(minLoadRetryCount) {
override fun getRetryDelayMsFor(loadErrorInfo: LoadErrorInfo): Long {
val errorMessage: String? = loadErrorInfo.exception.message
return if (loadErrorInfo.exception is HttpDataSourceException &&
errorMessage != null &&
(errorMessage == "Unable to connect" || errorMessage == "Software caused connection abort")
) {
// Capture the error we get when there is no network connectivity and keep retrying it
1000 // Retry every second
} else if (loadErrorInfo.errorCount < minLoadRetryCount) {
min(((loadErrorInfo.errorCount - 1) * 1000L), 5000L) // Default timeout handling
} else {
C.TIME_UNSET // Done retrying and will return the error immediately
}
}
override fun getMinimumLoadableRetryCount(dataType: Int): Int = Int.MAX_VALUE
}

View File

@@ -1,33 +0,0 @@
package com.brentvatne.exoplayer
import android.content.Context
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import java.io.File
object RNVSimpleCache {
// TODO: when to release? how to check if cache is released?
private var simpleCache: SimpleCache? = null
fun setSimpleCache(context: Context, cacheSize: Int) {
if (simpleCache != null || cacheSize <= 0) return
simpleCache = SimpleCache(
File(context.cacheDir, "RNVCache"),
LeastRecentlyUsedCacheEvictor(
cacheSize.toLong() * 1024 * 1024
),
StandaloneDatabaseProvider(context)
)
}
fun getCacheFactory(factory: HttpDataSource.Factory): DataSource.Factory {
if (simpleCache == null) return factory
return CacheDataSource.Factory()
.setCache(simpleCache!!)
.setUpstreamDataSourceFactory(factory)
}
}

View File

@@ -1,272 +0,0 @@
package com.brentvatne.exoplayer
import android.graphics.Color
import android.util.Log
import com.brentvatne.common.api.BufferingStrategy
import com.brentvatne.common.api.ControlsConfig
import com.brentvatne.common.api.ResizeMode
import com.brentvatne.common.api.Source
import com.brentvatne.common.api.SubtitleStyle
import com.brentvatne.common.api.ViewType
import com.brentvatne.common.react.EventTypes
import com.brentvatne.common.toolbox.DebugLog
import com.brentvatne.common.toolbox.ReactBridgeUtils
import com.brentvatne.react.ReactNativeVideoManager
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.annotations.ReactProp
class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : ViewGroupManager<ReactExoplayerView>() {
companion object {
private const val TAG = "ExoViewManager"
private const val REACT_CLASS = "RCTVideo"
private const val PROP_SRC = "src"
private const val PROP_RESIZE_MODE = "resizeMode"
private const val PROP_REPEAT = "repeat"
private const val PROP_SELECTED_AUDIO_TRACK = "selectedAudioTrack"
private const val PROP_SELECTED_AUDIO_TRACK_TYPE = "type"
private const val PROP_SELECTED_AUDIO_TRACK_VALUE = "value"
private const val PROP_SELECTED_TEXT_TRACK = "selectedTextTrack"
private const val PROP_SELECTED_TEXT_TRACK_TYPE = "type"
private const val PROP_SELECTED_TEXT_TRACK_VALUE = "value"
private const val PROP_PAUSED = "paused"
private const val PROP_ENTER_PICTURE_IN_PICTURE_ON_LEAVE = "enterPictureInPictureOnLeave"
private const val PROP_MUTED = "muted"
private const val PROP_AUDIO_OUTPUT = "audioOutput"
private const val PROP_VOLUME = "volume"
private const val PROP_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK =
"preventsDisplaySleepDuringVideoPlayback"
private const val PROP_PROGRESS_UPDATE_INTERVAL = "progressUpdateInterval"
private const val PROP_REPORT_BANDWIDTH = "reportBandwidth"
private const val PROP_RATE = "rate"
private const val PROP_MAXIMUM_BIT_RATE = "maxBitRate"
private const val PROP_PLAY_IN_BACKGROUND = "playInBackground"
private const val PROP_DISABLE_FOCUS = "disableFocus"
private const val PROP_BUFFERING_STRATEGY = "bufferingStrategy"
private const val PROP_DISABLE_DISCONNECT_ERROR = "disableDisconnectError"
private const val PROP_FOCUSABLE = "focusable"
private const val PROP_FULLSCREEN = "fullscreen"
private const val PROP_VIEW_TYPE = "viewType"
private const val PROP_SELECTED_VIDEO_TRACK = "selectedVideoTrack"
private const val PROP_SELECTED_VIDEO_TRACK_TYPE = "type"
private const val PROP_SELECTED_VIDEO_TRACK_VALUE = "value"
private const val PROP_HIDE_SHUTTER_VIEW = "hideShutterView"
private const val PROP_CONTROLS = "controls"
private const val PROP_SUBTITLE_STYLE = "subtitleStyle"
private const val PROP_SHUTTER_COLOR = "shutterColor"
private const val PROP_SHOW_NOTIFICATION_CONTROLS = "showNotificationControls"
private const val PROP_DEBUG = "debug"
private const val PROP_CONTROLS_STYLES = "controlsStyles"
}
override fun getName(): String = REACT_CLASS
override fun createViewInstance(themedReactContext: ThemedReactContext): ReactExoplayerView {
ReactNativeVideoManager.getInstance().registerView(this)
return ReactExoplayerView(themedReactContext, config)
}
override fun onDropViewInstance(view: ReactExoplayerView) {
view.cleanUpResources()
view.exitPictureInPictureMode()
ReactNativeVideoManager.getInstance().unregisterView(this)
}
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> = EventTypes.toMap()
override fun addEventEmitters(reactContext: ThemedReactContext, view: ReactExoplayerView) {
super.addEventEmitters(reactContext, view)
view.eventEmitter.addEventEmitters(reactContext, view)
}
@ReactProp(name = PROP_SRC)
fun setSrc(videoView: ReactExoplayerView, src: ReadableMap?) {
val context = videoView.context.applicationContext
videoView.setSrc(Source.parse(src, context))
}
@ReactProp(name = PROP_RESIZE_MODE)
fun setResizeMode(videoView: ReactExoplayerView, resizeMode: String) {
when (resizeMode) {
"none", "contain" -> videoView.setResizeModeModifier(ResizeMode.RESIZE_MODE_FIT)
"cover" -> videoView.setResizeModeModifier(ResizeMode.RESIZE_MODE_CENTER_CROP)
"stretch" -> videoView.setResizeModeModifier(ResizeMode.RESIZE_MODE_FILL)
else -> {
DebugLog.w(TAG, "Unsupported resize mode: $resizeMode - falling back to fit")
videoView.setResizeModeModifier(ResizeMode.RESIZE_MODE_FIT)
}
}
}
@ReactProp(name = PROP_REPEAT, defaultBoolean = false)
fun setRepeat(videoView: ReactExoplayerView, repeat: Boolean) {
videoView.setRepeatModifier(repeat)
}
@ReactProp(name = PROP_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK, defaultBoolean = false)
fun setPreventsDisplaySleepDuringVideoPlayback(videoView: ReactExoplayerView, preventsSleep: Boolean) {
videoView.preventsDisplaySleepDuringVideoPlayback = preventsSleep
}
@ReactProp(name = PROP_SELECTED_VIDEO_TRACK)
fun setSelectedVideoTrack(videoView: ReactExoplayerView, selectedVideoTrack: ReadableMap?) {
var typeString: String? = null
var value: String? = null
if (selectedVideoTrack != null) {
typeString = ReactBridgeUtils.safeGetString(selectedVideoTrack, PROP_SELECTED_VIDEO_TRACK_TYPE)
value = ReactBridgeUtils.safeGetString(selectedVideoTrack, PROP_SELECTED_VIDEO_TRACK_VALUE)
}
videoView.setSelectedVideoTrack(typeString, value)
}
@ReactProp(name = PROP_SELECTED_AUDIO_TRACK)
fun setSelectedAudioTrack(videoView: ReactExoplayerView, selectedAudioTrack: ReadableMap?) {
var typeString: String? = null
var value: String? = null
if (selectedAudioTrack != null) {
typeString = ReactBridgeUtils.safeGetString(selectedAudioTrack, PROP_SELECTED_AUDIO_TRACK_TYPE)
value = ReactBridgeUtils.safeGetString(selectedAudioTrack, PROP_SELECTED_AUDIO_TRACK_VALUE)
}
videoView.setSelectedAudioTrack(typeString, value)
}
@ReactProp(name = PROP_SELECTED_TEXT_TRACK)
fun setSelectedTextTrack(videoView: ReactExoplayerView, selectedTextTrack: ReadableMap?) {
var typeString: String? = null
var value: String? = null
if (selectedTextTrack != null) {
typeString = ReactBridgeUtils.safeGetString(selectedTextTrack, PROP_SELECTED_TEXT_TRACK_TYPE)
value = ReactBridgeUtils.safeGetString(selectedTextTrack, PROP_SELECTED_TEXT_TRACK_VALUE)
}
videoView.setSelectedTextTrack(typeString, value)
}
@ReactProp(name = PROP_PAUSED, defaultBoolean = false)
fun setPaused(videoView: ReactExoplayerView, paused: Boolean) {
videoView.setPausedModifier(paused)
}
@ReactProp(name = PROP_MUTED, defaultBoolean = false)
fun setMuted(videoView: ReactExoplayerView, muted: Boolean) {
videoView.setMutedModifier(muted)
}
@ReactProp(name = PROP_ENTER_PICTURE_IN_PICTURE_ON_LEAVE, defaultBoolean = false)
fun setEnterPictureInPictureOnLeave(videoView: ReactExoplayerView, enterPictureInPictureOnLeave: Boolean) {
videoView.setEnterPictureInPictureOnLeave(enterPictureInPictureOnLeave)
}
@ReactProp(name = PROP_AUDIO_OUTPUT)
fun setAudioOutput(videoView: ReactExoplayerView, audioOutput: String) {
videoView.setAudioOutput(AudioOutput.get(audioOutput))
}
@ReactProp(name = PROP_VOLUME, defaultFloat = 1.0f)
fun setVolume(videoView: ReactExoplayerView, volume: Float) {
videoView.setVolumeModifier(volume)
}
@ReactProp(name = PROP_PROGRESS_UPDATE_INTERVAL, defaultFloat = 250.0f)
fun setProgressUpdateInterval(videoView: ReactExoplayerView, progressUpdateInterval: Float) {
videoView.setProgressUpdateInterval(progressUpdateInterval)
}
@ReactProp(name = PROP_REPORT_BANDWIDTH, defaultBoolean = false)
fun setReportBandwidth(videoView: ReactExoplayerView, reportBandwidth: Boolean) {
videoView.setReportBandwidth(reportBandwidth)
}
@ReactProp(name = PROP_RATE)
fun setRate(videoView: ReactExoplayerView, rate: Float) {
videoView.setRateModifier(rate)
}
@ReactProp(name = PROP_MAXIMUM_BIT_RATE)
fun setMaxBitRate(videoView: ReactExoplayerView, maxBitRate: Float) {
videoView.setMaxBitRateModifier(maxBitRate.toInt())
}
@ReactProp(name = PROP_PLAY_IN_BACKGROUND, defaultBoolean = false)
fun setPlayInBackground(videoView: ReactExoplayerView, playInBackground: Boolean) {
videoView.setPlayInBackground(playInBackground)
}
@ReactProp(name = PROP_DISABLE_FOCUS, defaultBoolean = false)
fun setDisableFocus(videoView: ReactExoplayerView, disableFocus: Boolean) {
videoView.setDisableFocus(disableFocus)
}
@ReactProp(name = PROP_FOCUSABLE, defaultBoolean = true)
fun setFocusable(videoView: ReactExoplayerView, focusable: Boolean) {
videoView.setFocusable(focusable)
}
@ReactProp(name = PROP_BUFFERING_STRATEGY)
fun setBufferingStrategy(videoView: ReactExoplayerView, bufferingStrategy: String) {
val strategy = BufferingStrategy.parse(bufferingStrategy)
videoView.setBufferingStrategy(strategy)
}
@ReactProp(name = PROP_DISABLE_DISCONNECT_ERROR, defaultBoolean = false)
fun setDisableDisconnectError(videoView: ReactExoplayerView, disableDisconnectError: Boolean) {
videoView.setDisableDisconnectError(disableDisconnectError)
}
@ReactProp(name = PROP_FULLSCREEN, defaultBoolean = false)
fun setFullscreen(videoView: ReactExoplayerView, fullscreen: Boolean) {
videoView.setFullscreen(fullscreen)
}
@ReactProp(name = PROP_VIEW_TYPE, defaultInt = ViewType.VIEW_TYPE_SURFACE)
fun setViewType(videoView: ReactExoplayerView, viewType: Int) {
videoView.setViewType(viewType)
}
@ReactProp(name = PROP_HIDE_SHUTTER_VIEW, defaultBoolean = false)
fun setHideShutterView(videoView: ReactExoplayerView, hideShutterView: Boolean) {
videoView.setHideShutterView(hideShutterView)
}
@ReactProp(name = PROP_CONTROLS, defaultBoolean = false)
fun setControls(videoView: ReactExoplayerView, controls: Boolean) {
videoView.setControls(controls)
}
@ReactProp(name = PROP_SUBTITLE_STYLE)
fun setSubtitleStyle(videoView: ReactExoplayerView, src: ReadableMap?) {
videoView.setSubtitleStyle(SubtitleStyle.parse(src))
}
@ReactProp(name = PROP_SHUTTER_COLOR, defaultInt = Color.BLACK)
fun setShutterColor(videoView: ReactExoplayerView, color: Int) {
videoView.setShutterColor(color)
}
@ReactProp(name = PROP_SHOW_NOTIFICATION_CONTROLS)
fun setShowNotificationControls(videoView: ReactExoplayerView, showNotificationControls: Boolean) {
videoView.setShowNotificationControls(showNotificationControls)
}
@ReactProp(name = PROP_DEBUG, defaultBoolean = false)
fun setDebug(videoView: ReactExoplayerView, debugConfig: ReadableMap?) {
val enableDebug = ReactBridgeUtils.safeGetBool(debugConfig, "enable", false)
val enableThreadDebug = ReactBridgeUtils.safeGetBool(debugConfig, "thread", false)
if (enableDebug) {
DebugLog.setConfig(Log.VERBOSE, enableThreadDebug)
} else {
DebugLog.setConfig(Log.WARN, enableThreadDebug)
}
videoView.setDebug(enableDebug)
}
@ReactProp(name = PROP_CONTROLS_STYLES)
fun setControlsStyles(videoView: ReactExoplayerView, controlsStyles: ReadableMap?) {
val controlsConfig = ControlsConfig.parse(controlsStyles)
videoView.setControlsStyles(controlsConfig)
}
}

View File

@@ -1,43 +0,0 @@
package com.brentvatne.exoplayer
import android.os.Bundle
import androidx.media3.common.Player
import androidx.media3.session.MediaSession
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import com.brentvatne.exoplayer.VideoPlaybackService.Companion.COMMAND
import com.brentvatne.exoplayer.VideoPlaybackService.Companion.commandFromString
import com.brentvatne.exoplayer.VideoPlaybackService.Companion.handleCommand
import com.google.common.util.concurrent.ListenableFuture
class VideoPlaybackCallback : MediaSession.Callback {
override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
try {
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailablePlayerCommands(
MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
.add(Player.COMMAND_SEEK_FORWARD)
.add(Player.COMMAND_SEEK_BACK)
.build()
).setAvailableSessionCommands(
MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
.add(SessionCommand(COMMAND.SEEK_FORWARD.stringValue, Bundle.EMPTY))
.add(SessionCommand(COMMAND.SEEK_BACKWARD.stringValue, Bundle.EMPTY))
.build()
)
.build()
} catch (e: Exception) {
return MediaSession.ConnectionResult.reject()
}
}
override fun onCustomCommand(
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
handleCommand(commandFromString(customCommand.customAction), session)
return super.onCustomCommand(session, controller, customCommand, args)
}
}

View File

@@ -1,313 +0,0 @@
package com.brentvatne.exoplayer
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Binder
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.CommandButton
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import androidx.media3.session.MediaStyleNotificationHelper
import androidx.media3.session.SessionCommand
import com.brentvatne.common.toolbox.DebugLog
import com.brentvatne.react.R
import okhttp3.internal.immutableListOf
class PlaybackServiceBinder(val service: VideoPlaybackService) : Binder()
class VideoPlaybackService : MediaSessionService() {
private var mediaSessionsList = mutableMapOf<ExoPlayer, MediaSession>()
private var binder = PlaybackServiceBinder(this)
private var sourceActivity: Class<Activity>? = null
// Controls for Android 13+ - see buildNotification function
private val commandSeekForward = SessionCommand(COMMAND.SEEK_FORWARD.stringValue, Bundle.EMPTY)
private val commandSeekBackward = SessionCommand(COMMAND.SEEK_BACKWARD.stringValue, Bundle.EMPTY)
@SuppressLint("PrivateResource")
private val seekForwardBtn = CommandButton.Builder()
.setDisplayName("forward")
.setSessionCommand(commandSeekForward)
.setIconResId(androidx.media3.ui.R.drawable.exo_notification_fastforward)
.build()
@SuppressLint("PrivateResource")
private val seekBackwardBtn = CommandButton.Builder()
.setDisplayName("backward")
.setSessionCommand(commandSeekBackward)
.setIconResId(androidx.media3.ui.R.drawable.exo_notification_rewind)
.build()
// Player Registry
fun registerPlayer(player: ExoPlayer, from: Class<Activity>) {
if (mediaSessionsList.containsKey(player)) {
return
}
sourceActivity = from
val mediaSession = MediaSession.Builder(this, player)
.setId("RNVideoPlaybackService_" + player.hashCode())
.setCallback(VideoPlaybackCallback())
.setCustomLayout(immutableListOf(seekForwardBtn, seekBackwardBtn))
.build()
mediaSessionsList[player] = mediaSession
addSession(mediaSession)
val notificationId = player.hashCode()
startForeground(notificationId, buildNotification(mediaSession))
}
fun unregisterPlayer(player: ExoPlayer) {
hidePlayerNotification(player)
val session = mediaSessionsList.remove(player)
session?.release()
if (mediaSessionsList.isEmpty()) {
cleanup()
stopSelf()
}
}
// Callbacks
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = null
override fun onBind(intent: Intent?): IBinder {
super.onBind(intent)
return binder
}
override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) {
createSessionNotification(session)
}
override fun onTaskRemoved(rootIntent: Intent?) {
cleanup()
stopSelf()
}
override fun onDestroy() {
cleanup()
val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.deleteNotificationChannel(NOTIFICATION_CHANEL_ID)
}
super.onDestroy()
}
private fun createSessionNotification(session: MediaSession) {
val notificationManager: 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
)
)
}
if (session.player.currentMediaItem == null) {
notificationManager.cancel(session.player.hashCode())
return
}
val notification = buildNotification(session)
notificationManager.notify(session.player.hashCode(), notification)
}
private fun buildNotification(session: MediaSession): Notification {
val returnToPlayer = Intent(this, sourceActivity ?: this.javaClass).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
/*
* On Android 13+ controls are automatically handled via media session
* On Android 12 and bellow we need to add controls manually
*/
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID)
.setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
.setStyle(MediaStyleNotificationHelper.MediaStyle(session))
.setContentIntent(PendingIntent.getActivity(this, 0, returnToPlayer, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
.build()
} else {
val playerId = session.player.hashCode()
// Action for COMMAND.SEEK_BACKWARD
val seekBackwardIntent = Intent(this, VideoPlaybackService::class.java).apply {
putExtra("PLAYER_ID", playerId)
putExtra("ACTION", COMMAND.SEEK_BACKWARD.stringValue)
}
val seekBackwardPendingIntent = PendingIntent.getService(
this,
playerId * 10,
seekBackwardIntent,
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
// ACTION FOR COMMAND.TOGGLE_PLAY
val togglePlayIntent = Intent(this, VideoPlaybackService::class.java).apply {
putExtra("PLAYER_ID", playerId)
putExtra("ACTION", COMMAND.TOGGLE_PLAY.stringValue)
}
val togglePlayPendingIntent = PendingIntent.getService(
this,
playerId * 10 + 1,
togglePlayIntent,
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
// ACTION FOR COMMAND.SEEK_FORWARD
val seekForwardIntent = Intent(this, VideoPlaybackService::class.java).apply {
putExtra("PLAYER_ID", playerId)
putExtra("ACTION", COMMAND.SEEK_FORWARD.stringValue)
}
val seekForwardPendingIntent = PendingIntent.getService(
this,
playerId * 10 + 2,
seekForwardIntent,
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID)
// Show controls on lock screen even when user hides sensitive content.
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
// Add media control buttons that invoke intents in your media service
.addAction(androidx.media3.session.R.drawable.media3_icon_rewind, "Seek Backward", seekBackwardPendingIntent) // #0
.addAction(
if (session.player.isPlaying) {
androidx.media3.session.R.drawable.media3_icon_pause
} else {
androidx.media3.session.R.drawable.media3_icon_play
},
"Toggle Play",
togglePlayPendingIntent
) // #1
.addAction(androidx.media3.session.R.drawable.media3_icon_fast_forward, "Seek Forward", seekForwardPendingIntent) // #2
// Apply the media style template
.setStyle(MediaStyleNotificationHelper.MediaStyle(session).setShowActionsInCompactView(0, 1, 2))
.setContentTitle(session.player.mediaMetadata.title)
.setContentText(session.player.mediaMetadata.description)
.setContentIntent(PendingIntent.getActivity(this, 0, returnToPlayer, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
.setLargeIcon(session.player.mediaMetadata.artworkUri?.let { session.bitmapLoader.loadBitmap(it).get() })
.setOngoing(true)
.build()
}
}
private fun hidePlayerNotification(player: ExoPlayer) {
val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(player.hashCode())
}
private fun hideAllNotifications() {
val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancelAll()
}
private fun cleanup() {
hideAllNotifications()
mediaSessionsList.forEach { (_, session) ->
session.release()
}
mediaSessionsList.clear()
}
private fun createPlaceholderNotification(): Notification {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.createNotificationChannel(
NotificationChannel(
NOTIFICATION_CHANEL_ID,
NOTIFICATION_CHANEL_ID,
NotificationManager.IMPORTANCE_LOW
)
)
}
return NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID)
.setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
.setContentTitle(getString(R.string.media_playback_notification_title))
.setContentText(getString(R.string.media_playback_notification_text))
.build()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForeground(PLACEHOLDER_NOTIFICATION_ID, createPlaceholderNotification())
}
intent?.let {
val playerId = it.getIntExtra("PLAYER_ID", -1)
val actionCommand = it.getStringExtra("ACTION")
if (playerId < 0) {
DebugLog.w(TAG, "Received Command without playerId")
return super.onStartCommand(intent, flags, startId)
}
if (actionCommand == null) {
DebugLog.w(TAG, "Received Command without action command")
return super.onStartCommand(intent, flags, startId)
}
val session = mediaSessionsList.values.find { s -> s.player.hashCode() == playerId } ?: return super.onStartCommand(intent, flags, startId)
handleCommand(commandFromString(actionCommand), session)
}
return super.onStartCommand(intent, flags, startId)
}
companion object {
private const val SEEK_INTERVAL_MS = 10000L
private const val TAG = "VideoPlaybackService"
private const val PLACEHOLDER_NOTIFICATION_ID = 9999
const val NOTIFICATION_CHANEL_ID = "RNVIDEO_SESSION_NOTIFICATION"
enum class COMMAND(val stringValue: String) {
NONE("NONE"),
SEEK_FORWARD("COMMAND_SEEK_FORWARD"),
SEEK_BACKWARD("COMMAND_SEEK_BACKWARD"),
TOGGLE_PLAY("COMMAND_TOGGLE_PLAY"),
PLAY("COMMAND_PLAY"),
PAUSE("COMMAND_PAUSE")
}
fun commandFromString(value: String): COMMAND =
when (value) {
COMMAND.SEEK_FORWARD.stringValue -> COMMAND.SEEK_FORWARD
COMMAND.SEEK_BACKWARD.stringValue -> COMMAND.SEEK_BACKWARD
COMMAND.TOGGLE_PLAY.stringValue -> COMMAND.TOGGLE_PLAY
COMMAND.PLAY.stringValue -> COMMAND.PLAY
COMMAND.PAUSE.stringValue -> COMMAND.PAUSE
else -> COMMAND.NONE
}
fun handleCommand(command: COMMAND, session: MediaSession) {
// TODO: get somehow ControlsConfig here - for now hardcoded 10000ms
when (command) {
COMMAND.SEEK_BACKWARD -> session.player.seekTo(session.player.contentPosition - SEEK_INTERVAL_MS)
COMMAND.SEEK_FORWARD -> session.player.seekTo(session.player.contentPosition + SEEK_INTERVAL_MS)
COMMAND.TOGGLE_PLAY -> handleCommand(if (session.player.isPlaying) COMMAND.PAUSE else COMMAND.PLAY, session)
COMMAND.PLAY -> session.player.play()
COMMAND.PAUSE -> session.player.pause()
else -> DebugLog.w(TAG, "Received COMMAND.NONE - was there an error?")
}
}
}
}

View File

@@ -1,23 +0,0 @@
package com.brentvatne.react
/**
* Plugin interface definition for RNV plugins that does not have dependencies nor logic specific to any player
* It is the base interface for all RNV plugins
*/
interface RNVPlugin {
/**
* 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)
}

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