mirror of
https://github.com/zoriya/react-native-video.git
synced 2025-12-06 07:16:12 +00:00
chore: publish package
This commit is contained in:
15
.editorconfig
Normal file
15
.editorconfig
Normal 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
|
||||
@@ -1,2 +0,0 @@
|
||||
examples/
|
||||
lib/
|
||||
16
.eslintrc
16
.eslintrc
@@ -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
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.pbxproj -text
|
||||
# specific for windows script files
|
||||
*.bat text eol=crlf
|
||||
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,3 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: TheWidlarzGroup
|
||||
18
.github/ISSUE_TEMPLATE.md
vendored
18
.github/ISSUE_TEMPLATE.md
vendored
@@ -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.
|
||||
92
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
92
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -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
|
||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
11
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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.
|
||||
59
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
59
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -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)
|
||||
|
||||
|
||||
25
.github/PULL_REQUEST_TEMPLATE.md
vendored
25
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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
|
||||
34
.github/actions/setup-bun/action.yml
vendored
34
.github/actions/setup-bun/action.yml
vendored
@@ -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
|
||||
|
||||
34
.github/actions/setup-node/action.yml
vendored
34
.github/actions/setup-node/action.yml
vendored
@@ -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
27
.github/actions/setup/action.yml
vendored
Normal 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
|
||||
323
.github/scripts/validate.js
vendored
323
.github/scripts/validate.js
vendored
@@ -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;
|
||||
96
.github/workflows/build-android.yml
vendored
96
.github/workflows/build-android.yml
vendored
@@ -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 ../../..
|
||||
214
.github/workflows/build-ios.yml
vendored
214
.github/workflows/build-ios.yml
vendored
@@ -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"
|
||||
34
.github/workflows/check-android.yml
vendored
34
.github/workflows/check-android.yml
vendored
@@ -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
|
||||
31
.github/workflows/check-clang.yml
vendored
31
.github/workflows/check-clang.yml
vendored
@@ -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
|
||||
41
.github/workflows/check-ios.yml
vendored
41
.github/workflows/check-ios.yml
vendored
@@ -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
|
||||
62
.github/workflows/check-js.yml
vendored
62
.github/workflows/check-js.yml
vendored
@@ -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
157
.github/workflows/ci.yml
vendored
Normal 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 }}"
|
||||
46
.github/workflows/deploy-docs.yml
vendored
46
.github/workflows/deploy-docs.yml
vendored
@@ -1,46 +0,0 @@
|
||||
|
||||
name: deploy docs
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.github/workflows/deploy-docs.yml'
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
deploy-docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@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
|
||||
24
.github/workflows/stale.yml
vendored
24
.github/workflows/stale.yml
vendored
@@ -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 }}
|
||||
35
.github/workflows/test-build-docs.yml
vendored
35
.github/workflows/test-build-docs.yml
vendored
@@ -1,35 +0,0 @@
|
||||
|
||||
name: Test Docs build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/test-build-docs.yml'
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
build-docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@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
|
||||
19
.github/workflows/validate-issue.yml
vendored
19
.github/workflows/validate-issue.yml
vendored
@@ -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
60
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
echo "precommit"
|
||||
yarn check-all
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": false,
|
||||
"jsxBracketSameLine": true
|
||||
}
|
||||
@@ -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
1
.watchmanconfig
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
541
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
541
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
874
.yarn/releases/yarn-3.6.1.cjs
vendored
Executable file
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
10
.yarnrc.yml
Normal 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
|
||||
1046
CHANGELOG.md
1046
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
133
CODE_OF_CONDUCT.md
Normal file
133
CODE_OF_CONDUCT.md
Normal 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
|
||||
145
CONTRIBUTING.md
145
CONTRIBUTING.md
@@ -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
48
LICENSE
@@ -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.
|
||||
@@ -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
125
README.md
@@ -1,122 +1,35 @@
|
||||
[](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 pay‑per‑issue 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) | Plug‑and‑play secure download solution for iOS & Android |
|
||||
| [**Integration Support**](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=readme&utm_campaign=integration-support#Contact) | Hands‑on help integrating video, DRM & offline into your app |
|
||||
| [**Free DRM Token Generator**](https://www.thewidlarzgroup.com/services/free-drm-token-generator-for-video?utm_source=rnv&utm_medium=readme&utm_id=free-drm) | Generate Widevine / FairPlay tokens for testing |
|
||||
| [**Ready Boilerplates**](https://www.thewidlarzgroup.com/showcases?utm_source=rnv&utm_medium=readme) | Ready-to-use apps with offline HLS/DASH DRM, video frame scrubbing, TikTok-style video feed, background uploads, Skia-based frame processor (R&D phase), and more |
|
||||
| [**React Native Video Upgrade Guide**](https://www.thewidlarzgroup.com/blog/react-native-video-upgrade-challenges-custom-maintenance-support?utm_source=rnv&utm_medium=readme&utm_id=upgrade-blog&utm_campaign=v7) | Common upgrade pitfalls & how to solve them |
|
||||
|
||||
*See how [TWG](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=readme&utm_id=services-text) helped **Learnn** ship a world‑class player in record time - [case study](https://gitnation.com/contents/a-4-year-retrospective-lessons-learned-from-building-a-video-player-from-scratch-with-react-native).*
|
||||
|
||||
Contact us at [hi@thewidlarzgroup.com](mailto:hi@thewidlarzgroup.com)
|
||||
|
||||
## 🌍 Social
|
||||
|
||||
- 🐦 **X / Twitter** - [follow product & release updates](https://x.com/TheWidlarzGroup)
|
||||
- 💬 **Discord** - [talk to the community and us](https://discord.gg/9WPq6Yx)
|
||||
- 💼 **LinkedIn** - [see TWG flexing](https://linkedin.com/company/the-widlarz-group)
|
||||
|
||||
## 📰 Community & Media
|
||||
|
||||
- 🗽 **React Summit US** – How TWG helped Learnn boost video performance on React Native.
|
||||
[Watch the talk »](https://gitnation.com/contents/a-4-year-retrospective-lessons-learned-from-building-a-video-player-from-scratch-with-react-native)
|
||||
|
||||
- 🧨 **v7 deep dive** – Why we’re building v7 with Nitro Modules
|
||||
[Watch on X »](https://x.com/krzysztof_moch/status/1854162551946478051)
|
||||
|
||||
- 🛠️ **Well-maintained open-source library** - What does it truly mean? - Bart's talk for React Native Warsaw
|
||||
[Watch here »](https://www.youtube.com/watch?v=RAQQwGCQNqY)
|
||||
|
||||
- 📺 **“Over the Top” Panel** - Building Streaming Apps for Mobile, Web, and Smart TVs - Bart giving his insights on the industry
|
||||
[Watch here »](https://youtu.be/j2b_bG-32JI)
|
||||
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
|
||||
|
||||
5334
THIRD-PARTY-LICENSES
Normal file
5334
THIRD-PARTY-LICENSES
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
29
android/CMakeLists.txt
Normal 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
|
||||
)
|
||||
@@ -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 Android’s MediaPlayer API for playing audio and video both locally and over the Internet. ExoPlayer supports features not currently supported by Android’s 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 }}`
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -1,3 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.brentvatne.react">
|
||||
package="com.video">
|
||||
</manifest>
|
||||
|
||||
6
android/src/main/cpp/cpp-adapter.cpp
Normal file
6
android/src/main/cpp/cpp-adapter.cpp
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package androidx.media3.exoplayer.dash.manifest;
|
||||
|
||||
import androidx.collection.CircularArray;
|
||||
|
||||
public class Period {
|
||||
public CircularArray<AdaptationSet> adaptationSets;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)"
|
||||
}
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user