Merge remote-tracking branch 'upstream/master'
Some checks failed
Build Android / Build Android Example App (push) Has been cancelled
Build Android / Build Android Example App With Ads (push) Has been cancelled
Build iOS / Build iOS Example App (push) Has been cancelled
Build iOS / Build iOS Example App With Ads (push) Has been cancelled
Build iOS / Build iOS Example App With Caching (push) Has been cancelled
Check Android / Kotlin-Lint (push) Has been cancelled
Check CLang / CLang-Format (push) Has been cancelled
Check iOS / Swift-Lint (push) Has been cancelled
Check iOS / Swift-Format (push) Has been cancelled
Check JS / Check TS (tsc) (push) Has been cancelled
Check JS / Lint JS (eslint, prettier) (push) Has been cancelled
deploy docs / deploy-docs (push) Has been cancelled
Some checks failed
Build Android / Build Android Example App (push) Has been cancelled
Build Android / Build Android Example App With Ads (push) Has been cancelled
Build iOS / Build iOS Example App (push) Has been cancelled
Build iOS / Build iOS Example App With Ads (push) Has been cancelled
Build iOS / Build iOS Example App With Caching (push) Has been cancelled
Check Android / Kotlin-Lint (push) Has been cancelled
Check CLang / CLang-Format (push) Has been cancelled
Check iOS / Swift-Lint (push) Has been cancelled
Check iOS / Swift-Format (push) Has been cancelled
Check JS / Check TS (tsc) (push) Has been cancelled
Check JS / Lint JS (eslint, prettier) (push) Has been cancelled
deploy docs / deploy-docs (push) Has been cancelled
This commit is contained in:
commit
bb9e08c43a
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: TheWidlarzGroup
|
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@ -74,7 +74,7 @@ body:
|
||||
- type: input
|
||||
id: reproduction-repo
|
||||
attributes:
|
||||
label: Reproduction
|
||||
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"
|
||||
|
13
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
13
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@ -7,7 +7,7 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thanks for taking the time to fill out this feature report!
|
||||
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
@ -17,7 +17,7 @@ body:
|
||||
value: "Very cool idea!"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
- type: textarea
|
||||
id: why-it-is-needed
|
||||
attributes:
|
||||
@ -49,4 +49,11 @@ body:
|
||||
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#Contact) - [`hi@thewidlarzgroup.com`](mailto:hi@thewidlarzgroup.com)
|
||||
|
||||
|
||||
|
2
.github/actions/setup-bun/action.yml
vendored
2
.github/actions/setup-bun/action.yml
vendored
@ -17,7 +17,7 @@ runs:
|
||||
|
||||
- name: Cache dependencies
|
||||
id: bun-cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
|
2
.github/actions/setup-node/action.yml
vendored
2
.github/actions/setup-node/action.yml
vendored
@ -17,7 +17,7 @@ runs:
|
||||
|
||||
- name: Cache dependencies
|
||||
id: yarn-cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ inputs.working-directory }}/node_modules
|
||||
|
297
.github/scripts/validate.js
vendored
Normal file
297
.github/scripts/validate.js
vendored
Normal file
@ -0,0 +1,297 @@
|
||||
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',
|
||||
...Object.values(PLATFORM_LABELS),
|
||||
];
|
||||
|
||||
const SKIP_LABEL = 'No Validation';
|
||||
|
||||
const MESSAGE = {
|
||||
FEATURE_REQUEST: `Thanks for the feature request! Check out our roadmap [here](https://github.com/TheWidlarzGroup/react-native-video/discussions/3351). If your request is already there – great! If not, give us some time, and we'll get back to you with information on when TheWidlarzGroup can address it as part of our free open-source support. Alternatively, [contact us](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=feature-request#Contact) to discuss ways to speed up the process.`,
|
||||
BUG_REPORT: `Thank you for your bug report. We will review it and get back to you if we need more information.`,
|
||||
MISSING_INFO: (missingFields) => {
|
||||
return `Thank you for your issue report. Please note that the following information is missing or incomplete:\n\n${missingFields
|
||||
.map((field) => `- ${field.replace('missing-', '')}`)
|
||||
.join(
|
||||
'\n',
|
||||
)}\n\nPlease update your issue with this information to help us address it more effectively.
|
||||
\n > Note: issues without complete information have a lower priority`;
|
||||
},
|
||||
OUTDATED_VERSION: (issueVersion, latestVersion) => {
|
||||
return (
|
||||
`There is a newer version of the library available. ` +
|
||||
`You are using version ${issueVersion}, while the latest stable version is ${latestVersion}. ` +
|
||||
`Please update to the latest version and check if the issue still exists.` +
|
||||
`\n > Note: If the issue still exists, please update the issue report with the latest information.`
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
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;
|
||||
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,
|
||||
)}`;
|
||||
}
|
||||
|
||||
await hidePreviousComments({github, context});
|
||||
await createComment({github, context, body: comment});
|
||||
}
|
||||
|
||||
updateLabelsForMissingInfo(labels);
|
||||
};
|
||||
|
||||
const handleValidReport = async ({github, context, labels}) => {
|
||||
let comment = MESSAGE.BUG_REPORT;
|
||||
|
||||
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;
|
60
.github/stale.yml
vendored
60
.github/stale.yml
vendored
@ -1,60 +0,0 @@
|
||||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 60
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: 3
|
||||
|
||||
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels: []
|
||||
|
||||
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
|
||||
# Set to true to ignore issues in a project (defaults to false)
|
||||
exemptProjects: true
|
||||
|
||||
# Set to true to ignore issues in a milestone (defaults to false)
|
||||
exemptMilestones: true
|
||||
|
||||
# Set to true to ignore issues with an assignee (defaults to false)
|
||||
exemptAssignees: true
|
||||
|
||||
# Label to use when marking as stale
|
||||
staleLabel: stale
|
||||
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions. If you are having a similar problem, please open a
|
||||
new issue and reference this one instead of commenting on a stale or closed
|
||||
issue.
|
||||
|
||||
# Comment to post when removing the stale label.
|
||||
unmarkComment: false
|
||||
|
||||
# Comment to post when closing a stale Issue or Pull Request.
|
||||
closeComment: false
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 50
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
only: issues
|
||||
|
||||
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
|
||||
# pulls:
|
||||
# daysUntilStale: 30
|
||||
# markComment: >
|
||||
# This pull request has been automatically marked as stale because it has not had
|
||||
# recent activity. It will be closed if no further activity occurs. Thank you
|
||||
# for your contributions.
|
||||
|
||||
# issues:
|
||||
# exemptLabels:
|
||||
# - confirmed
|
48
.github/workflows/build-android.yml
vendored
48
.github/workflows/build-android.yml
vendored
@ -7,16 +7,16 @@ on:
|
||||
paths:
|
||||
- '.github/workflows/build-android.yml'
|
||||
- 'android/**'
|
||||
- 'examples/basic/android/**'
|
||||
- 'examples/bare/android/**'
|
||||
- 'yarn.lock'
|
||||
- 'examples/basic/yarn.lock'
|
||||
- 'examples/bare/yarn.lock'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/build-android.yml'
|
||||
- 'android/**'
|
||||
- 'examples/basic/android/**'
|
||||
- 'examples/bare/android/**'
|
||||
- 'yarn.lock'
|
||||
- 'examples/basic/yarn.lock'
|
||||
- 'examples/bare/yarn.lock'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@ -32,13 +32,21 @@ jobs:
|
||||
java-version: 17
|
||||
java-package: jdk
|
||||
|
||||
- name: Install node_modules
|
||||
- name: Install node_modules at Root
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
working-directory: examples/basic
|
||||
working-directory: ./
|
||||
|
||||
- name: Build Library
|
||||
run: yarn build
|
||||
|
||||
- name: Install node_modules at Example
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
working-directory: examples/bare
|
||||
|
||||
- name: Restore Gradle cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@ -46,11 +54,11 @@ jobs:
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
- name: Run Gradle Build for basic example
|
||||
run: cd examples/basic/android && ./gradlew assembleDebug --build-cache && cd ../../..
|
||||
- name: Run Gradle Build for bare example
|
||||
run: cd examples/bare/android && ./gradlew assembleDebug --build-cache && cd ../../..
|
||||
|
||||
build-without-ads:
|
||||
name: Build Android Example App Without Ads
|
||||
build-with-ads:
|
||||
name: Build Android Example App With Ads
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -62,13 +70,21 @@ jobs:
|
||||
java-version: 17
|
||||
java-package: jdk
|
||||
|
||||
- name: Install node_modules
|
||||
- name: Install node_modules at Root
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
working-directory: examples/basic
|
||||
working-directory: ./
|
||||
|
||||
- name: Build Library
|
||||
run: yarn build
|
||||
|
||||
- name: Install node_modules at Example
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
working-directory: examples/bare
|
||||
|
||||
- name: Restore Gradle cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@ -76,5 +92,5 @@ jobs:
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
- name: Run Gradle Build for basic example
|
||||
run: cd examples/basic/android && export RNV_SAMPLE_ENABLE_ADS=false && ./gradlew assembleDebug --build-cache && cd ../../..
|
||||
- name: Run Gradle Build for bare example
|
||||
run: cd examples/bare/android && export RNV_SAMPLE_ENABLE_ADS=true && ./gradlew assembleDebug --build-cache && cd ../../..
|
||||
|
97
.github/workflows/build-ios.yml
vendored
97
.github/workflows/build-ios.yml
vendored
@ -9,28 +9,37 @@ on:
|
||||
- '.github/workflows/build-ios.yml'
|
||||
- 'ios/**'
|
||||
- '*.podspec'
|
||||
- 'examples/basic/ios/**'
|
||||
- 'examples/bare/ios/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/build-ios.yml'
|
||||
- 'ios/**'
|
||||
- '*.podspec'
|
||||
- 'examples/basic/ios/**'
|
||||
- 'examples/bare/ios/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build iOS Example App
|
||||
runs-on: macos-14 # This allow us to use Xcode 15.0.1 which is a lot faster - TODO change to "macos-latest" once it's out of beta
|
||||
runs-on: macos-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: examples/basic/ios
|
||||
working-directory: examples/bare/ios
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install node_modules
|
||||
- name: Install node_modules at Root
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
working-directory: examples/basic
|
||||
working-directory: ./
|
||||
|
||||
- name: Build Library
|
||||
working-directory: ./
|
||||
run: yarn build
|
||||
|
||||
- name: Install node_modules at Example
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
working-directory: examples/bare
|
||||
|
||||
- name: Restore buildcache
|
||||
uses: mikehardy/buildcache-action@v2
|
||||
@ -43,24 +52,30 @@ jobs:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Restore Pods cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
examples/basic/ios/Pods
|
||||
examples/bare/ios/Pods
|
||||
~/Library/Caches/CocoaPods
|
||||
~/.cocoapods
|
||||
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pods-
|
||||
|
||||
- name: Generate Native Project
|
||||
run: pod install
|
||||
|
||||
- name: Install Pods
|
||||
run: pod install
|
||||
|
||||
- name: Install xcpretty
|
||||
run: gem install xcpretty
|
||||
|
||||
- name: Build App
|
||||
run: "set -o pipefail && xcodebuild \
|
||||
-derivedDataPath build -UseModernBuildSystem=YES \
|
||||
-workspace videoplayer.xcworkspace \
|
||||
-scheme videoplayer \
|
||||
-workspace BareExample.xcworkspace \
|
||||
-scheme BareExample \
|
||||
-sdk iphonesimulator \
|
||||
-configuration Debug \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 14' \
|
||||
@ -69,17 +84,26 @@ jobs:
|
||||
|
||||
build-with-ads:
|
||||
name: Build iOS Example App With Ads
|
||||
runs-on: macos-14 # This allow us to use Xcode 15.0.1 which is a lot faster - TODO change to "macos-latest" once it's out of beta
|
||||
runs-on: macos-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: examples/basic/ios
|
||||
working-directory: examples/bare/ios
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install node_modules
|
||||
- name: Install node_modules at Root
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
working-directory: examples/basic
|
||||
working-directory: ./
|
||||
|
||||
- name: Build Library
|
||||
working-directory: ./
|
||||
run: yarn build
|
||||
|
||||
- name: Install node_modules at Example
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
working-directory: examples/bare
|
||||
|
||||
- name: Restore buildcache
|
||||
uses: mikehardy/buildcache-action@v2
|
||||
@ -92,24 +116,30 @@ jobs:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Restore Pods cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
examples/basic/ios/Pods
|
||||
examples/bare/ios/Pods
|
||||
~/Library/Caches/CocoaPods
|
||||
~/.cocoapods
|
||||
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pods-
|
||||
|
||||
- name: Generate Native Project
|
||||
run: export RNV_SAMPLE_ENABLE_ADS=true && pod install
|
||||
|
||||
- name: Install Pods
|
||||
run: export RNV_SAMPLE_ENABLE_ADS=true && pod install
|
||||
|
||||
- name: Install xcpretty
|
||||
run: gem install xcpretty
|
||||
|
||||
- name: Build App
|
||||
run: "set -o pipefail && export RNV_SAMPLE_ENABLE_ADS=true && xcodebuild \
|
||||
-derivedDataPath build -UseModernBuildSystem=YES \
|
||||
-workspace videoplayer.xcworkspace \
|
||||
-scheme videoplayer \
|
||||
-workspace BareExample.xcworkspace \
|
||||
-scheme BareExample \
|
||||
-sdk iphonesimulator \
|
||||
-configuration Debug \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 14' \
|
||||
@ -118,17 +148,26 @@ jobs:
|
||||
|
||||
build-with-caching:
|
||||
name: Build iOS Example App With Caching
|
||||
runs-on: macos-14 # This allow us to use Xcode 15.0.1 which is a lot faster - TODO change to "macos-latest" once it's out of beta
|
||||
runs-on: macos-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: examples/basic/ios
|
||||
working-directory: examples/bare/ios
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install node_modules
|
||||
- name: Install node_modules at Root
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
working-directory: examples/basic
|
||||
working-directory: ./
|
||||
|
||||
- name: Build Library
|
||||
working-directory: ./
|
||||
run: yarn build
|
||||
|
||||
- name: Install node_modules at Example
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
working-directory: examples/bare
|
||||
|
||||
- name: Restore buildcache
|
||||
uses: mikehardy/buildcache-action@v2
|
||||
@ -141,24 +180,30 @@ jobs:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Restore Pods cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
examples/basic/ios/Pods
|
||||
examples/bare/ios/Pods
|
||||
~/Library/Caches/CocoaPods
|
||||
~/.cocoapods
|
||||
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pods-
|
||||
|
||||
- name: Generate Native Project
|
||||
run: export RNV_SAMPLE_VIDEO_CACHING=true && pod install
|
||||
|
||||
- name: Install Pods
|
||||
run: export RNV_SAMPLE_VIDEO_CACHING=true && pod install
|
||||
|
||||
- name: Install xcpretty
|
||||
run: gem install xcpretty
|
||||
|
||||
- name: Build App
|
||||
run: "set -o pipefail && export RNV_SAMPLE_VIDEO_CACHING=true && xcodebuild \
|
||||
-derivedDataPath build -UseModernBuildSystem=YES \
|
||||
-workspace videoplayer.xcworkspace \
|
||||
-scheme videoplayer \
|
||||
-workspace BareExample.xcworkspace \
|
||||
-scheme BareExample \
|
||||
-sdk iphonesimulator \
|
||||
-configuration Debug \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 14' \
|
||||
|
4
.github/workflows/deploy-docs.yml
vendored
4
.github/workflows/deploy-docs.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup-bun
|
||||
@ -22,7 +22,7 @@ jobs:
|
||||
working-directory: ./docs
|
||||
|
||||
- name: Cache build
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
docs/.next/cache
|
||||
|
24
.github/workflows/stale.yml
vendored
Normal file
24
.github/workflows/stale.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
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 }}
|
4
.github/workflows/test-build-docs.yml
vendored
4
.github/workflows/test-build-docs.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup-bun
|
||||
@ -20,7 +20,7 @@ jobs:
|
||||
working-directory: ./docs
|
||||
|
||||
- name: Cache build
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
docs/.next/cache
|
||||
|
19
.github/workflows/validate-issue.yml
vendored
Normal file
19
.github/workflows/validate-issue.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
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})
|
164
CHANGELOG.md
164
CHANGELOG.md
@ -1,5 +1,169 @@
|
||||
|
||||
|
||||
## [6.9.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.9.0...v6.9.1) (2025-01-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* avoid memory leak on iOS ([#4355](https://github.com/TheWidlarzGroup/react-native-video/issues/4355)) ([424f4ee](https://github.com/TheWidlarzGroup/react-native-video/commit/424f4eeddea989392e25c52f45a9a0281ead6fe1))
|
||||
* NPE in setEnterPictureInPictureOnLeave for unsupported Android versions ([#4362](https://github.com/TheWidlarzGroup/react-native-video/issues/4362)) ([3924b5e](https://github.com/TheWidlarzGroup/react-native-video/commit/3924b5e295ed64c97284f4665bc294066a83574a))
|
||||
|
||||
# [6.9.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.8.2...v6.9.0) (2025-01-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **android:** disable caching on local asset files ([#4304](https://github.com/TheWidlarzGroup/react-native-video/issues/4304)) ([63c592f](https://github.com/TheWidlarzGroup/react-native-video/commit/63c592f7cd897caf918fd3bd5f129c72432d2b55))
|
||||
* **docs:** bump `next.js` version & fix meta warnings ([#4327](https://github.com/TheWidlarzGroup/react-native-video/issues/4327)) ([7b4bd9a](https://github.com/TheWidlarzGroup/react-native-video/commit/7b4bd9a0169fc2ea6f277dd7ed904bada98bc63a))
|
||||
* hiding poster ([#4308](https://github.com/TheWidlarzGroup/react-native-video/issues/4308)) ([621a802](https://github.com/TheWidlarzGroup/react-native-video/commit/621a80299c690c07846f3fcd8a6c73b7ecde39bf))
|
||||
* **ios:** `_paused` is updated when video playback pause ([#4320](https://github.com/TheWidlarzGroup/react-native-video/issues/4320)) ([3da4f1c](https://github.com/TheWidlarzGroup/react-native-video/commit/3da4f1ca979058b387b1be2c2141f6b93fd084a7))
|
||||
* **ios:** disables subtitles for `none` and `empty` track types ([#4319](https://github.com/TheWidlarzGroup/react-native-video/issues/4319)) ([1033c9d](https://github.com/TheWidlarzGroup/react-native-video/commit/1033c9d4f3db7042a96e7108a7fe9f1567d69ded))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* implement enterPictureInPictureOnLeave prop for both platform (Android, iOS) ([#3385](https://github.com/TheWidlarzGroup/react-native-video/issues/3385)) ([69a7bc2](https://github.com/TheWidlarzGroup/react-native-video/commit/69a7bc2d265f2cf4985f8d81054c46f47ee3bae2))
|
||||
|
||||
## [6.8.2](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.8.1...v6.8.2) (2024-11-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* playback restart without bufferingConfig ([#4305](https://github.com/TheWidlarzGroup/react-native-video/issues/4305)) ([f37dc9e](https://github.com/TheWidlarzGroup/react-native-video/commit/f37dc9e33ebefd922605c5ae91360379fe91bed6))
|
||||
|
||||
## [6.8.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.8.0...v6.8.1) (2024-11-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ios:** handle async player access in text track selection ([#4293](https://github.com/TheWidlarzGroup/react-native-video/issues/4293)) ([daaac97](https://github.com/TheWidlarzGroup/react-native-video/commit/daaac9740aed1858b7ababae0ec8b08274130a27))
|
||||
|
||||
# [6.8.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.7.0...v6.8.0) (2024-11-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **android:** add helper to avoid type error ([#4257](https://github.com/TheWidlarzGroup/react-native-video/issues/4257)) ([3b4bfd3](https://github.com/TheWidlarzGroup/react-native-video/commit/3b4bfd3936a8cb846c0e61ffd396940987a7ba43))
|
||||
|
||||
# [6.7.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.6.4...v6.7.0) (2024-10-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **android:** sideloaded subtitles ([#4232](https://github.com/TheWidlarzGroup/react-native-video/issues/4232)) ([352dfbb](https://github.com/TheWidlarzGroup/react-native-video/commit/352dfbbc9bef158400c59516a50d889d25757c0d))
|
||||
* ensure aspect ratio from video is handled in a coherent way ([#4219](https://github.com/TheWidlarzGroup/react-native-video/issues/4219)) ([a8d5841](https://github.com/TheWidlarzGroup/react-native-video/commit/a8d5841c7c0f9767ec095ffd8401b1579f32623f))
|
||||
* **iOS:** pause video on end reached & don't remove listeners ([#4218](https://github.com/TheWidlarzGroup/react-native-video/issues/4218)) ([2c19a47](https://github.com/TheWidlarzGroup/react-native-video/commit/2c19a4770df73179436a9e23a5e55ad0699fcfcc))
|
||||
* remove warning and refactor & fix ad workflow ([#4235](https://github.com/TheWidlarzGroup/react-native-video/issues/4235)) ([7501880](https://github.com/TheWidlarzGroup/react-native-video/commit/7501880062a4c381838949084f0017a2aecc58d7))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add setSource API function fix ads playback ([#4185](https://github.com/TheWidlarzGroup/react-native-video/issues/4185)) ([9a3fcda](https://github.com/TheWidlarzGroup/react-native-video/commit/9a3fcda3b8ca4689c9131a12a8375fc43d442f80))
|
||||
* **android:** add settings button to control video playback speed ([#4211](https://github.com/TheWidlarzGroup/react-native-video/issues/4211)) ([d1883a7](https://github.com/TheWidlarzGroup/react-native-video/commit/d1883a7e008706cac2a2beac934194539c9b5b77))
|
||||
* **exoplayerview:** Migrate ExoPlayerView to kotlin ([#4038](https://github.com/TheWidlarzGroup/react-native-video/issues/4038)) ([78f4f04](https://github.com/TheWidlarzGroup/react-native-video/commit/78f4f0480d70d209fea9e0579963e347c965fd6e))
|
||||
|
||||
## [6.6.4](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.6.3...v6.6.4) (2024-10-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **android:** add live video label configuration ([#4190](https://github.com/TheWidlarzGroup/react-native-video/issues/4190)) ([149924f](https://github.com/TheWidlarzGroup/react-native-video/commit/149924ffcb0cbdeaa8c671ebb4b3b6115920131a))
|
||||
|
||||
## [6.6.3](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.6.2...v6.6.3) (2024-09-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **android:** bad rotation handling ([#4205](https://github.com/TheWidlarzGroup/react-native-video/issues/4205)) ([3ecf324](https://github.com/TheWidlarzGroup/react-native-video/commit/3ecf324bb30208ab8efbf00958ebd4590ddf8d39))
|
||||
* **docs:** invalid URLs in updating section ([#4201](https://github.com/TheWidlarzGroup/react-native-video/issues/4201)) ([c81eea5](https://github.com/TheWidlarzGroup/react-native-video/commit/c81eea54d8291c5131fd59a93f198e0fd5f3673c))
|
||||
* **ios:** Add safety checks and remove some of the ! in types declaration ([#4182](https://github.com/TheWidlarzGroup/react-native-video/issues/4182)) ([ae82c83](https://github.com/TheWidlarzGroup/react-native-video/commit/ae82c83eef2fc7c383fd844c7471613e4ac1c7ee))
|
||||
* **tvos:** typo ([#4204](https://github.com/TheWidlarzGroup/react-native-video/issues/4204)) ([b11f05f](https://github.com/TheWidlarzGroup/react-native-video/commit/b11f05f1753a4cb963b94d1e1d8d1f6c37af2a9d))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **android:** allow to hide specific controls ([#4183](https://github.com/TheWidlarzGroup/react-native-video/issues/4183)) ([279cc0e](https://github.com/TheWidlarzGroup/react-native-video/commit/279cc0e5ed712488fc3c153c62b14f13048103f2))
|
||||
|
||||
## [6.6.2](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.6.1...v6.6.2) (2024-09-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **iOS:** rewrite DRM Module ([#4136](https://github.com/TheWidlarzGroup/react-native-video/issues/4136)) ([0e4c95d](https://github.com/TheWidlarzGroup/react-native-video/commit/0e4c95def968a4091fdd18d07215ba592eec99cb))
|
||||
|
||||
## [6.6.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.6.0...v6.6.1) (2024-09-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ios:** fix side loaded text track management ([#4180](https://github.com/TheWidlarzGroup/react-native-video/issues/4180)) ([7d43d5d](https://github.com/TheWidlarzGroup/react-native-video/commit/7d43d5d3da72495e94468756be21442f96cc7a89))
|
||||
|
||||
# [6.6.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.5.0...v6.6.0) (2024-09-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **android:** ensure maxbitrate & selectedVideoTrack interact correctly ([#4155](https://github.com/TheWidlarzGroup/react-native-video/issues/4155)) ([7f6b500](https://github.com/TheWidlarzGroup/react-native-video/commit/7f6b500c82122325c326b6dcacaf7af8039b2b33))
|
||||
* **android:** ensure pause is well tken in account after onEnd ([#4147](https://github.com/TheWidlarzGroup/react-native-video/issues/4147)) ([b2fd8d6](https://github.com/TheWidlarzGroup/react-native-video/commit/b2fd8d62a10ee64e6208b43120ca9231008309c2))
|
||||
* **expo-plugin:** add check for existing service in AndroidManifest for notification controls ([#4172](https://github.com/TheWidlarzGroup/react-native-video/issues/4172)) ([0538b3b](https://github.com/TheWidlarzGroup/react-native-video/commit/0538b3b46801a535c76cf52db28cee76f2aeb0c5))
|
||||
* **ios:** ensure onBandwidthUpdate is reported only when value change ([#4149](https://github.com/TheWidlarzGroup/react-native-video/issues/4149)) ([809a730](https://github.com/TheWidlarzGroup/react-native-video/commit/809a73019836f95385891c2bba5c72b0610ffcb1))
|
||||
* **ios:** losing subtitle selection on foreground ([#3707](https://github.com/TheWidlarzGroup/react-native-video/issues/3707)) ([bee4123](https://github.com/TheWidlarzGroup/react-native-video/commit/bee4123402f4bc08dd2eb19ab0011ffdc795d0e3))
|
||||
* **JS:** improve loader api to allow function call instead of component ([#4171](https://github.com/TheWidlarzGroup/react-native-video/issues/4171)) ([835186a](https://github.com/TheWidlarzGroup/react-native-video/commit/835186a321e1940932a045a59e26e43a040fa334))
|
||||
* refactor side loaded text tracks management ([#4158](https://github.com/TheWidlarzGroup/react-native-video/issues/4158)) ([84a27f3](https://github.com/TheWidlarzGroup/react-native-video/commit/84a27f3d9f90624af3c5c3cbff50d754bab9baa4))
|
||||
* **sample:** remove warning on ios with NavigationBar ([#4148](https://github.com/TheWidlarzGroup/react-native-video/issues/4148)) ([e18769a](https://github.com/TheWidlarzGroup/react-native-video/commit/e18769ab3a6a7f4ebc459ab550f105f4d18f8201))
|
||||
* **visionOS:** remove unsupported apis ([#4154](https://github.com/TheWidlarzGroup/react-native-video/issues/4154)) ([2c1fc96](https://github.com/TheWidlarzGroup/react-native-video/commit/2c1fc964bf2cb97624c8cc37ff8138465619fc61))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **android:** upgrade dependencies / media3 1.4.1 / androidxCore to 1.13.1 / androidxActivity 1.8.2 ([#4173](https://github.com/TheWidlarzGroup/react-native-video/issues/4173)) ([e57c7bd](https://github.com/TheWidlarzGroup/react-native-video/commit/e57c7bda5df7d624d90b20620859b8a4eb3f76b7))
|
||||
|
||||
# [6.5.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.4.5...v6.5.0) (2024-09-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **android:** show the status bar and navigation bar after exiting full-screen mode ([#4112](https://github.com/TheWidlarzGroup/react-native-video/issues/4112)) ([8b8ebe9](https://github.com/TheWidlarzGroup/react-native-video/commit/8b8ebe9410e95085e5602393c2ce3de814df4a96))
|
||||
* **android:** add subtitleStyle.subtitlesFollowVideo prop to control subtitles positionning ([#4133](https://github.com/TheWidlarzGroup/react-native-video/issues/4133)) ([2fa6c43](https://github.com/TheWidlarzGroup/react-native-video/commit/2fa6c43615c1bc0a3bbcb5f472ffaeb8ae16a1af))
|
||||
* **android:** hide surfaceView for loading time when shutter is hidden ([#4060](https://github.com/TheWidlarzGroup/react-native-video/issues/4060)) ([65faba3](https://github.com/TheWidlarzGroup/react-native-video/commit/65faba312d23de981972d2b6ffecefbc87ecac61))
|
||||
* **expo-plugin:** adding bg mode if none exist yet ([#4126](https://github.com/TheWidlarzGroup/react-native-video/issues/4126)) ([451806c](https://github.com/TheWidlarzGroup/react-native-video/commit/451806c547591fbe5714b133e704ffac9efb05d8))
|
||||
* **ios:** Add handler for Earpods play/pause command ([#4116](https://github.com/TheWidlarzGroup/react-native-video/issues/4116)) ([9c38d9f](https://github.com/TheWidlarzGroup/react-native-video/commit/9c38d9f4ef42c3e275ee39a08aa227e6b976fdb2))
|
||||
* **ios:** build fail due to an unwrapped value ([#4101](https://github.com/TheWidlarzGroup/react-native-video/issues/4101)) ([0a1085c](https://github.com/TheWidlarzGroup/react-native-video/commit/0a1085ce03152d58d98da408dbe79e76fa5ebc1a))
|
||||
* **ios:** ensure behavior is correct with empty text track list ([#4123](https://github.com/TheWidlarzGroup/react-native-video/issues/4123)) ([3a32d67](https://github.com/TheWidlarzGroup/react-native-video/commit/3a32d67087c39bcf7904043d15a2fdba65307f4e))
|
||||
* **ios:** ensure we don't disable tracks when not necessary (causes black screen) ([#4130](https://github.com/TheWidlarzGroup/react-native-video/issues/4130)) ([89df9d6](https://github.com/TheWidlarzGroup/react-native-video/commit/89df9d69ff96f7d6ff3d493bf1a3eb9c3da51c3c))
|
||||
* **ios:** fix onBandwidth update event (old ios api is deprecated and doens't work) ([#4140](https://github.com/TheWidlarzGroup/react-native-video/issues/4140)) ([d6bae3c](https://github.com/TheWidlarzGroup/react-native-video/commit/d6bae3cd076018f07556ab27af2779479bc7ff7d))
|
||||
* **sample:** update dependencies to fix local asset playback ([#4121](https://github.com/TheWidlarzGroup/react-native-video/issues/4121)) ([7a2b401](https://github.com/TheWidlarzGroup/react-native-video/commit/7a2b4014f40758a025fcd6b388448d3559ec6a4a))
|
||||
* set does not have `find` method ([#4110](https://github.com/TheWidlarzGroup/react-native-video/issues/4110)) ([7db7024](https://github.com/TheWidlarzGroup/react-native-video/commit/7db7024cb36ea34289fddf5c7f66e7b4d7827146))
|
||||
* **tvos:** fix build (and update sample) ([#4134](https://github.com/TheWidlarzGroup/react-native-video/issues/4134)) ([688d98d](https://github.com/TheWidlarzGroup/react-native-video/commit/688d98d68f888a59bde1ee33aa844ac63c9026a5))
|
||||
* **VisionOS:** do not access to isExternalPlaybackActive on VisionOS ([#4109](https://github.com/TheWidlarzGroup/react-native-video/issues/4109)) ([0576eac](https://github.com/TheWidlarzGroup/react-native-video/commit/0576eacfddb32c4dcc072b6fd3cbf74cf25946a4))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add ads localize ([#4113](https://github.com/TheWidlarzGroup/react-native-video/issues/4113)) ([703ed43](https://github.com/TheWidlarzGroup/react-native-video/commit/703ed4399667e0142704d19686563dd62fb4883d))
|
||||
* **android:** Support Common Media Client Data (CMCD) ([#4034](https://github.com/TheWidlarzGroup/react-native-video/issues/4034)) ([ca795f2](https://github.com/TheWidlarzGroup/react-native-video/commit/ca795f298a99a183b81561ef7e09d8d1e8addaf5))
|
||||
* **android:** support hiding Exoplayer video duration on android ([#4090](https://github.com/TheWidlarzGroup/react-native-video/issues/4090)) ([41e2bed](https://github.com/TheWidlarzGroup/react-native-video/commit/41e2bed6b36f74a28d7dd640414c6d5ccbec0399))
|
||||
* Correct isBehindLiveWindow Error Handling ([#4143](https://github.com/TheWidlarzGroup/react-native-video/issues/4143)) ([22c21ad](https://github.com/TheWidlarzGroup/react-native-video/commit/22c21ad249879fe4ff8fb119384ebc82766106c3))
|
||||
|
||||
## [6.4.5](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.4.4...v6.4.5) (2024-08-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **android:** resolve a release issue with DefaultDashChunkSource ([#4097](https://github.com/TheWidlarzGroup/react-native-video/issues/4097)) ([7e222e8](https://github.com/TheWidlarzGroup/react-native-video/commit/7e222e8fc4f3c47a1c9cd2fbf5ff012bcbe98a7f))
|
||||
|
||||
* refactor(android): migrate DefaultDashChunkSource to Kotlin (#4078) (b7d1cabf)
|
||||
* fix(ios): remove resume logic in notification seek closure (#4068) (c6ae17e4)
|
||||
* chore(doc): update document (props & method) (#4072) (cd41a1b2)
|
||||
* fix(android): build warnings (#4058) (899bb822)
|
||||
* infra: update feature request form (#4065) (6c03d0a7)
|
||||
* fix(ios): override source metadata with custom metadata (#4050) (38aa2b05)
|
||||
* fix(android): return the value as a float for the getCurrentPosition function (#4054) (af0302b1)
|
||||
* refactor(android): migrate ReactExoplayerViewManager to Kotlin (#4011) (74c6dd62)
|
||||
* fix(android): viewType is ignored when its value is ViewType.TEXTURE (#4031) (22cfd6ce)
|
||||
* fix(ios): metadata update race (#4033) (08a57a3b)
|
||||
* fix(ios): updated getLicense call to work with new syntax, and fixed spelling error (#4014) (#4042) (2348c5e4)
|
||||
|
||||
## [6.4.3](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.4.2...v6.4.3) (2024-07-24)
|
||||
|
||||
|
||||
|
@ -24,8 +24,30 @@ For example, a single file JS code change requires 1 review while a 3 files iOS
|
||||
|
||||
* If you have time to help out, look for the [`review requested`](https://github.com/TheWidlarzGroup/react-native-video/labels/review%20requested) label. It will have another numeric label with it (`1`, `2`, or `3` indicating how many more reviews are needed to merge).
|
||||
|
||||
## Releases
|
||||
|
||||
* Aim for a bi-weekly (every other week) release to flush out whatever was approved and merge. Most people use this with a lock file (and if you don't you are doing it wrong) and should not have any issues with new bugs showing up. This is already a high risk dependency which must be tested well before going into production. Let's take advantage of that and move faster.
|
||||
|
||||
Please do not harass people to review your pull request! You can tag those you feel have relevant experience but please don't abuse this as people will unfollow or mute the project if they are called too many times!
|
||||
|
||||
### Running the example
|
||||
|
||||
To see how to run examples locally, please refer to the [examples guide](https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples)
|
||||
|
||||
### Working on documentation
|
||||
|
||||
The documentation is located in the `docs` folder. To work on the documentation, you can run the following command to start a local server:
|
||||
|
||||
```sh
|
||||
cd docs
|
||||
bun install
|
||||
bun dev
|
||||
```
|
||||
|
||||
### Publishing a release
|
||||
|
||||
We use [release-it](https://github.com/webpro/release-it) to automate our release.
|
||||
|
||||
## Reporting issues
|
||||
|
||||
You can report issues on our [bug tracker](https://github.com/TheWidlarzGroup/react-native-video/issues). Please follow the issue template when opening an issue.
|
||||
|
||||
## License
|
||||
|
||||
By contributing to React Native Video, you agree that your contributions will be licensed under its **MIT** license.
|
||||
|
10
README.md
10
README.md
@ -2,7 +2,11 @@
|
||||
🎬 `<Video>` component for React Native
|
||||
|
||||
## Documentation
|
||||
documentation is available at [thewidlarzgroup.github.io/react-native-video/](https://thewidlarzgroup.github.io/react-native-video/)
|
||||
documentation is available at [docs.thewidlarzgroup.com/react-native-video/](https://docs.thewidlarzgroup.com/react-native-video/)
|
||||
|
||||
## Examples
|
||||
You can find several examples demonstrating the usage of react-native-video [here](https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples). <br />
|
||||
These include a [basic](https://github.com/TheWidlarzGroup/react-native-video/blob/master/examples/bare/src/BasicExample.tsx) usage and [DRM example](https://github.com/TheWidlarzGroup/react-native-video/blob/master/examples/bare/src/DRMExample.tsx) (with a [free DRM stream](https://www.thewidlarzgroup.com/services/free-drm-token-generator-for-video?utm_source=drm&utm_medium=code)).
|
||||
|
||||
## Usage
|
||||
|
||||
@ -51,9 +55,9 @@ We have an discord server where you can ask questions and get help. [Join the di
|
||||
|
||||
## Enterprise Support
|
||||
<p>
|
||||
📱 <i>react-native-video</i> is provided <i>as it is</i>. For enterprise support or other business inquiries, <a href="https://www.thewidlarzgroup.com/">please contact us 🤝</a>. We can help you with the integration, customization and maintenance. We are providing both free and commercial support for this project. let's build something awesome together! 🚀
|
||||
📱 <i>react-native-video</i> is provided <i>as it is</i>. For enterprise support or other business inquiries, <a href="https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=readme#Contact">please contact us 🤝</a>. We can help you with the integration, customization and maintenance. We are providing both free and commercial support for this project. let's build something awesome together! 🚀
|
||||
</p>
|
||||
<a href="https://www.thewidlarzgroup.com/">
|
||||
<a href="https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=readme">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./docs/assets/baners/twg-dark.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./docs/assets/baners/twg-light.png" />
|
||||
|
@ -91,9 +91,10 @@ def configStringPath = ExoplayerDependencies
|
||||
.concat("buildFromSource:$media3_buildFromSource")
|
||||
.md5()
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
apply plugin: "com.facebook.react"
|
||||
}
|
||||
// commented as new architecture not yet fully supported
|
||||
// if (isNewArchitectureEnabled()) {
|
||||
// apply plugin: "com.facebook.react"
|
||||
// }
|
||||
|
||||
android {
|
||||
if (supportsNamespace()) {
|
||||
@ -216,7 +217,7 @@ dependencies {
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation "com.facebook.react:react-native:+"
|
||||
|
||||
implementation "androidx.core:core:$androidxCore_version"
|
||||
implementation "androidx.core:core-ktx:$androidxCore_version"
|
||||
implementation "androidx.activity:activity-ktx:$androidxActivity_version"
|
||||
|
||||
// For media playback using ExoPlayer
|
||||
|
@ -4,12 +4,12 @@ RNVideo_targetSdkVersion=34
|
||||
RNVideo_compileSdkVersion=34
|
||||
RNVideo_ndkversion=26.1.10909125
|
||||
RNVideo_buildToolsVersion=34.0.0
|
||||
RNVideo_media3Version=1.3.1
|
||||
RNVideo_media3Version=1.4.1
|
||||
RNVideo_useExoplayerIMA=false
|
||||
RNVideo_useExoplayerRtsp=false
|
||||
RNVideo_useExoplayerSmoothStreaming=true
|
||||
RNVideo_useExoplayerDash=true
|
||||
RNVideo_useExoplayerHls=true
|
||||
RNVideo_androidxCoreVersion=1.9.0
|
||||
RNVideo_androidxActivityVersion=1.7.0
|
||||
RNVideo_androidxCoreVersion=1.13.1
|
||||
RNVideo_androidxActivityVersion=1.9.3
|
||||
RNVideo_buildFromMedia3Source=false
|
||||
|
@ -7,4 +7,4 @@ public class DefaultDashChunkSource {
|
||||
public Factory(DataSource.Factory mediaDataSourceFactory) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -11,9 +11,17 @@ 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) {
|
||||
}
|
||||
|
||||
@ -45,6 +53,7 @@ public class ImaAdsLoader implements AdsLoader {
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private ImaSdkSettings imaSdkSettings;
|
||||
public Builder(Context ignoredThemedReactContext) {
|
||||
}
|
||||
|
||||
@ -56,6 +65,11 @@ public class ImaAdsLoader implements AdsLoader {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) {
|
||||
this.imaSdkSettings = imaSdkSettings;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ImaAdsLoader build() {
|
||||
return null;
|
||||
}
|
||||
|
43
android/src/main/java/com/brentvatne/common/api/AdsProps.kt
Normal file
43
android/src/main/java/com/brentvatne/common/api/AdsProps.kt
Normal file
@ -0,0 +1,43 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -23,6 +23,23 @@ class BufferConfig {
|
||||
|
||||
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 &&
|
||||
live == other.live
|
||||
)
|
||||
}
|
||||
|
||||
class Live {
|
||||
var maxPlaybackSpeed: Float = BufferConfigPropUnsetDouble.toFloat()
|
||||
var minPlaybackSpeed: Float = BufferConfigPropUnsetDouble.toFloat()
|
||||
@ -30,12 +47,23 @@ class BufferConfig {
|
||||
var minOffsetMs: Long = BufferConfigPropUnsetInt.toLong()
|
||||
var targetOffsetMs: Long = BufferConfigPropUnsetInt.toLong()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other == null || other !is Live) return false
|
||||
return (
|
||||
maxPlaybackSpeed == other.maxPlaybackSpeed &&
|
||||
minPlaybackSpeed == other.minPlaybackSpeed &&
|
||||
maxOffsetMs == other.maxOffsetMs &&
|
||||
minOffsetMs == other.minOffsetMs &&
|
||||
targetOffsetMs == other.targetOffsetMs
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val PROP_BUFFER_CONFIG_LIVE_MAX_PLAYBACK_SPEED = "maxPlaybackSpeed"
|
||||
private val PROP_BUFFER_CONFIG_LIVE_MIN_PLAYBACK_SPEED = "minPlaybackSpeed"
|
||||
private val PROP_BUFFER_CONFIG_LIVE_MAX_OFFSET_MS = "maxOffsetMs"
|
||||
private val PROP_BUFFER_CONFIG_LIVE_MIN_OFFSET_MS = "minOffsetMs"
|
||||
private val PROP_BUFFER_CONFIG_LIVE_TARGET_OFFSET_MS = "targetOffsetMs"
|
||||
private const val PROP_BUFFER_CONFIG_LIVE_MAX_PLAYBACK_SPEED = "maxPlaybackSpeed"
|
||||
private const val PROP_BUFFER_CONFIG_LIVE_MIN_PLAYBACK_SPEED = "minPlaybackSpeed"
|
||||
private const val PROP_BUFFER_CONFIG_LIVE_MAX_OFFSET_MS = "maxOffsetMs"
|
||||
private const val PROP_BUFFER_CONFIG_LIVE_MIN_OFFSET_MS = "minOffsetMs"
|
||||
private const val PROP_BUFFER_CONFIG_LIVE_TARGET_OFFSET_MS = "targetOffsetMs"
|
||||
|
||||
@JvmStatic
|
||||
fun parse(src: ReadableMap?): Live {
|
||||
@ -54,16 +82,16 @@ class BufferConfig {
|
||||
val BufferConfigPropUnsetInt = -1
|
||||
val BufferConfigPropUnsetDouble = -1.0
|
||||
|
||||
private val PROP_BUFFER_CONFIG_CACHE_SIZE = "cacheSizeMB"
|
||||
private val PROP_BUFFER_CONFIG_MIN_BUFFER_MS = "minBufferMs"
|
||||
private val PROP_BUFFER_CONFIG_MAX_BUFFER_MS = "maxBufferMs"
|
||||
private val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs"
|
||||
private val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = "bufferForPlaybackAfterRebufferMs"
|
||||
private val PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT = "maxHeapAllocationPercent"
|
||||
private val PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT = "minBackBufferMemoryReservePercent"
|
||||
private val PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT = "minBufferMemoryReservePercent"
|
||||
private val PROP_BUFFER_CONFIG_BACK_BUFFER_DURATION_MS = "backBufferDurationMs"
|
||||
private val PROP_BUFFER_CONFIG_LIVE = "live"
|
||||
private const val PROP_BUFFER_CONFIG_CACHE_SIZE = "cacheSizeMB"
|
||||
private const val PROP_BUFFER_CONFIG_MIN_BUFFER_MS = "minBufferMs"
|
||||
private const val PROP_BUFFER_CONFIG_MAX_BUFFER_MS = "maxBufferMs"
|
||||
private const val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs"
|
||||
private const val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = "bufferForPlaybackAfterRebufferMs"
|
||||
private const val PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT = "maxHeapAllocationPercent"
|
||||
private const val PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT = "minBackBufferMemoryReservePercent"
|
||||
private const val PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT = "minBufferMemoryReservePercent"
|
||||
private const val PROP_BUFFER_CONFIG_BACK_BUFFER_DURATION_MS = "backBufferDurationMs"
|
||||
private const val PROP_BUFFER_CONFIG_LIVE = "live"
|
||||
|
||||
@JvmStatic
|
||||
fun parse(src: ReadableMap?): BufferConfig {
|
||||
|
51
android/src/main/java/com/brentvatne/common/api/CMCDProps.kt
Normal file
51
android/src/main/java/com/brentvatne/common/api/CMCDProps.kt
Normal file
@ -0,0 +1,51 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,18 +5,42 @@ 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(src: ReadableMap?): ControlsConfig {
|
||||
fun parse(controlsConfig: ReadableMap?): ControlsConfig {
|
||||
val config = ControlsConfig()
|
||||
|
||||
if (src != null) {
|
||||
config.hideSeekBar = ReactBridgeUtils.safeGetBool(src, "hideSeekBar", false)
|
||||
config.seekIncrementMS = ReactBridgeUtils.safeGetInt(src, "seekIncrementMS", 10000)
|
||||
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,8 +1,7 @@
|
||||
package com.brentvatne.common.api
|
||||
|
||||
import androidx.annotation.IntDef
|
||||
import java.lang.annotation.Retention
|
||||
import java.lang.annotation.RetentionPolicy
|
||||
import kotlin.annotation.Retention
|
||||
|
||||
internal object ResizeMode {
|
||||
/**
|
||||
@ -42,7 +41,7 @@ internal object ResizeMode {
|
||||
else -> RESIZE_MODE_FIT
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@IntDef(
|
||||
RESIZE_MODE_FIT,
|
||||
RESIZE_MODE_FIXED_WIDTH,
|
||||
|
@ -11,12 +11,18 @@ import com.facebook.react.bridge.ReadableMap
|
||||
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
|
||||
}
|
||||
var sideLoadedTextTrackList = SideLoadedTextTrackList()
|
||||
val sideLoadedTextTrackList = SideLoadedTextTrackList()
|
||||
for (i in 0 until src.size()) {
|
||||
val textTrack: ReadableMap = src.getMap(i)
|
||||
sideLoadedTextTrackList.tracks.add(SideLoadedTextTrack.parse(textTrack))
|
||||
|
@ -14,6 +14,7 @@ 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
|
||||
@ -29,6 +30,12 @@ class Source {
|
||||
/** Parsed value of source to playback */
|
||||
var uri: Uri? = null
|
||||
|
||||
/** True if source is a local JS asset */
|
||||
var isLocalAssetFile: Boolean = false
|
||||
|
||||
/** True if source is a local file asset://, ... */
|
||||
var isAsset: Boolean = false
|
||||
|
||||
/** Start position of playback used to resume playback */
|
||||
var startPositionMs: Int = -1
|
||||
|
||||
@ -38,12 +45,18 @@ class Source {
|
||||
/** 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()
|
||||
|
||||
@ -57,6 +70,26 @@ class Source {
|
||||
*/
|
||||
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 */
|
||||
@ -68,7 +101,15 @@ class Source {
|
||||
cropEndMs == other.cropEndMs &&
|
||||
startPositionMs == other.startPositionMs &&
|
||||
extension == other.extension &&
|
||||
drmProps == other.drmProps
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
@ -124,14 +165,22 @@ class Source {
|
||||
companion object {
|
||||
private const val TAG = "Source"
|
||||
private const val PROP_SRC_URI = "uri"
|
||||
private const val PROP_SRC_IS_LOCAL_ASSET_FILE = "isLocalAssetFile"
|
||||
private const val PROP_SRC_IS_ASSET = "isAsset"
|
||||
private const val PROP_SRC_START_POSITION = "startPosition"
|
||||
private const val PROP_SRC_CROP_START = "cropStart"
|
||||
private const val PROP_SRC_CROP_END = "cropEnd"
|
||||
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? {
|
||||
@ -184,12 +233,22 @@ class 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) {
|
||||
|
@ -6,7 +6,7 @@ import com.facebook.react.bridge.ReadableMap
|
||||
/**
|
||||
* Helper file to parse SubtitleStyle prop and build a dedicated class
|
||||
*/
|
||||
class SubtitleStyle private constructor() {
|
||||
class SubtitleStyle public constructor() {
|
||||
var fontSize = -1
|
||||
private set
|
||||
var paddingLeft = 0
|
||||
@ -19,6 +19,8 @@ class SubtitleStyle private constructor() {
|
||||
private set
|
||||
var opacity = 1f
|
||||
private set
|
||||
var subtitlesFollowVideo = true
|
||||
private set
|
||||
|
||||
companion object {
|
||||
private const val PROP_FONT_SIZE_TRACK = "fontSize"
|
||||
@ -27,6 +29,7 @@ class SubtitleStyle private constructor() {
|
||||
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 {
|
||||
@ -37,6 +40,7 @@ class SubtitleStyle private constructor() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,8 @@ enum class EventTypes(val eventName: String) {
|
||||
|
||||
EVENT_TEXT_TRACK_DATA_CHANGED("onTextTrackDataChanged"),
|
||||
EVENT_VIDEO_TRACKS("onVideoTracks"),
|
||||
EVENT_ON_RECEIVE_AD_EVENT("onReceiveAdEvent");
|
||||
EVENT_ON_RECEIVE_AD_EVENT("onReceiveAdEvent"),
|
||||
EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED("onPictureInPictureStatusChanged");
|
||||
|
||||
companion object {
|
||||
fun toMap() =
|
||||
@ -65,11 +66,11 @@ class VideoEventEmitter {
|
||||
audioTracks: ArrayList<Track>,
|
||||
textTracks: ArrayList<Track>,
|
||||
videoTracks: ArrayList<VideoTrack>,
|
||||
trackId: String
|
||||
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 onVideoBandwidthUpdate: (bitRateEstimate: Long, height: Int, width: Int, trackId: String?) -> Unit
|
||||
lateinit var onVideoPlaybackStateChanged: (isPlaying: Boolean, isSeeking: Boolean) -> Unit
|
||||
lateinit var onVideoSeek: (currentPosition: Long, seekTime: Long) -> Unit
|
||||
lateinit var onVideoSeekComplete: (currentPosition: Long) -> Unit
|
||||
@ -92,6 +93,7 @@ class VideoEventEmitter {
|
||||
lateinit var onVideoTracks: (videoTracks: ArrayList<VideoTrack>?) -> Unit
|
||||
lateinit var onTextTrackDataChanged: (textTrackData: String) -> Unit
|
||||
lateinit var onReceiveAdEvent: (adEvent: String, adData: Map<String?, String?>?) -> Unit
|
||||
lateinit var onPictureInPictureStatusChanged: (isActive: Boolean) -> Unit
|
||||
|
||||
fun addEventEmitters(reactContext: ThemedReactContext, view: ReactExoplayerView) {
|
||||
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
|
||||
@ -110,7 +112,7 @@ class VideoEventEmitter {
|
||||
|
||||
val naturalSize: WritableMap = aspectRatioToNaturalSize(videoWidth, videoHeight)
|
||||
putMap("naturalSize", naturalSize)
|
||||
putString("trackId", trackId)
|
||||
trackId?.let { putString("trackId", it) }
|
||||
putArray("videoTracks", videoTracksToArray(videoTracks))
|
||||
putArray("audioTracks", audioTracksToArray(audioTracks))
|
||||
putArray("textTracks", textTracksToArray(textTracks))
|
||||
@ -155,9 +157,13 @@ class VideoEventEmitter {
|
||||
onVideoBandwidthUpdate = { bitRateEstimate, height, width, trackId ->
|
||||
event.dispatch(EventTypes.EVENT_BANDWIDTH) {
|
||||
putDouble("bitrate", bitRateEstimate.toDouble())
|
||||
putInt("width", width)
|
||||
putInt("height", height)
|
||||
putString("trackId", trackId)
|
||||
if (width > 0) {
|
||||
putInt("width", width)
|
||||
}
|
||||
if (height > 0) {
|
||||
putInt("height", height)
|
||||
}
|
||||
trackId?.let { putString("trackId", it) }
|
||||
}
|
||||
}
|
||||
onVideoPlaybackStateChanged = { isPlaying, isSeeking ->
|
||||
@ -216,7 +222,7 @@ class VideoEventEmitter {
|
||||
putArray(
|
||||
"metadata",
|
||||
Arguments.createArray().apply {
|
||||
metadataArrayList.forEachIndexed { i, metadata ->
|
||||
metadataArrayList.forEachIndexed { _, metadata ->
|
||||
pushMap(
|
||||
Arguments.createMap().apply {
|
||||
putString("identifier", metadata.identifier)
|
||||
@ -281,6 +287,11 @@ class VideoEventEmitter {
|
||||
)
|
||||
}
|
||||
}
|
||||
onPictureInPictureStatusChanged = { isActive ->
|
||||
event.dispatch(EventTypes.EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED) {
|
||||
putBoolean("isActive", isActive)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,7 +321,7 @@ class VideoEventEmitter {
|
||||
|
||||
private fun videoTracksToArray(videoTracks: java.util.ArrayList<VideoTrack>?): WritableArray =
|
||||
Arguments.createArray().apply {
|
||||
videoTracks?.forEachIndexed { i, vTrack ->
|
||||
videoTracks?.forEachIndexed { _, vTrack ->
|
||||
pushMap(
|
||||
Arguments.createMap().apply {
|
||||
putInt("width", vTrack.width)
|
||||
@ -343,15 +354,19 @@ class VideoEventEmitter {
|
||||
|
||||
private fun aspectRatioToNaturalSize(videoWidth: Int, videoHeight: Int): WritableMap =
|
||||
Arguments.createMap().apply {
|
||||
putInt("width", videoWidth)
|
||||
putInt("height", videoHeight)
|
||||
val orientation = if (videoWidth > videoHeight) {
|
||||
"landscape"
|
||||
} else if (videoWidth < videoHeight) {
|
||||
"portrait"
|
||||
} else {
|
||||
"square"
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package com.brentvatne.common.toolbox
|
||||
import com.facebook.react.bridge.Dynamic
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import java.util.HashMap
|
||||
|
||||
/*
|
||||
* Toolbox to safe parsing of <Video props
|
||||
@ -54,6 +53,17 @@ object ReactBridgeUtils {
|
||||
|
||||
@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.
|
||||
*
|
||||
|
@ -2,6 +2,7 @@ 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
|
||||
|
||||
@ -94,4 +95,12 @@ class AspectRatioFrameLayout(context: Context) : FrameLayout(context) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
54
android/src/main/java/com/brentvatne/exoplayer/CMCDConfig.kt
Normal file
54
android/src/main/java/com/brentvatne/exoplayer/CMCDConfig.kt
Normal file
@ -0,0 +1,54 @@
|
||||
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,292 +0,0 @@
|
||||
package com.brentvatne.exoplayer;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.media3.common.AdViewProvider;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
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.exoplayer.ExoPlayer;
|
||||
import androidx.media3.ui.SubtitleView;
|
||||
|
||||
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 com.brentvatne.common.api.ResizeMode;
|
||||
import com.brentvatne.common.api.SubtitleStyle;
|
||||
import com.brentvatne.common.api.ViewType;
|
||||
import com.brentvatne.common.toolbox.DebugLog;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
public final class ExoPlayerView extends FrameLayout implements AdViewProvider {
|
||||
private final static String TAG = "ExoPlayerView";
|
||||
private View surfaceView;
|
||||
private final View shutterView;
|
||||
private final SubtitleView subtitleLayout;
|
||||
private final AspectRatioFrameLayout layout;
|
||||
private final ComponentListener componentListener;
|
||||
private ExoPlayer player;
|
||||
private final Context context;
|
||||
private final ViewGroup.LayoutParams layoutParams;
|
||||
private final FrameLayout adOverlayFrameLayout;
|
||||
|
||||
private @ViewType.ViewType int viewType = ViewType.VIEW_TYPE_SURFACE;
|
||||
private boolean hideShutterView = false;
|
||||
|
||||
public ExoPlayerView(Context context) {
|
||||
super(context, null, 0);
|
||||
|
||||
this.context = context;
|
||||
|
||||
layoutParams = new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
|
||||
componentListener = new ComponentListener();
|
||||
|
||||
FrameLayout.LayoutParams aspectRatioParams = new FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT);
|
||||
aspectRatioParams.gravity = Gravity.CENTER;
|
||||
layout = new AspectRatioFrameLayout(context);
|
||||
layout.setLayoutParams(aspectRatioParams);
|
||||
|
||||
shutterView = new View(getContext());
|
||||
shutterView.setLayoutParams(layoutParams);
|
||||
shutterView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.black));
|
||||
|
||||
subtitleLayout = new SubtitleView(context);
|
||||
subtitleLayout.setLayoutParams(layoutParams);
|
||||
subtitleLayout.setUserDefaultStyle();
|
||||
subtitleLayout.setUserDefaultTextSize();
|
||||
|
||||
updateSurfaceView(viewType);
|
||||
|
||||
adOverlayFrameLayout = new FrameLayout(context);
|
||||
|
||||
layout.addView(shutterView, 1, layoutParams);
|
||||
layout.addView(adOverlayFrameLayout, 2, layoutParams);
|
||||
|
||||
addViewInLayout(layout, 0, aspectRatioParams);
|
||||
addViewInLayout(subtitleLayout, 1, layoutParams);
|
||||
}
|
||||
|
||||
private void clearVideoView() {
|
||||
if (surfaceView instanceof TextureView) {
|
||||
player.clearVideoTextureView((TextureView) surfaceView);
|
||||
} else if (surfaceView instanceof SurfaceView) {
|
||||
player.clearVideoSurfaceView((SurfaceView) surfaceView);
|
||||
}
|
||||
}
|
||||
|
||||
private void setVideoView() {
|
||||
if (surfaceView instanceof TextureView) {
|
||||
player.setVideoTextureView((TextureView) surfaceView);
|
||||
} else if (surfaceView instanceof SurfaceView) {
|
||||
player.setVideoSurfaceView((SurfaceView) surfaceView);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isPlaying() {
|
||||
return player != null && player.isPlaying();
|
||||
}
|
||||
|
||||
public void setSubtitleStyle(SubtitleStyle style) {
|
||||
// ensure we reset subtile style before reapplying it
|
||||
subtitleLayout.setUserDefaultStyle();
|
||||
subtitleLayout.setUserDefaultTextSize();
|
||||
|
||||
if (style.getFontSize() > 0) {
|
||||
subtitleLayout.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, style.getFontSize());
|
||||
}
|
||||
subtitleLayout.setPadding(style.getPaddingLeft(), style.getPaddingTop(), style.getPaddingRight(), style.getPaddingBottom());
|
||||
if (style.getOpacity() != 0) {
|
||||
subtitleLayout.setAlpha(style.getOpacity());
|
||||
subtitleLayout.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
subtitleLayout.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void setShutterColor(Integer color) {
|
||||
shutterView.setBackgroundColor(color);
|
||||
}
|
||||
|
||||
public void updateSurfaceView(@ViewType.ViewType int viewType) {
|
||||
this.viewType = viewType;
|
||||
boolean viewNeedRefresh = false;
|
||||
if (viewType == ViewType.VIEW_TYPE_SURFACE || viewType == ViewType.VIEW_TYPE_SURFACE_SECURE) {
|
||||
if (!(surfaceView instanceof SurfaceView)) {
|
||||
surfaceView = new SurfaceView(context);
|
||||
viewNeedRefresh = true;
|
||||
}
|
||||
((SurfaceView)surfaceView).setSecure(viewType == ViewType.VIEW_TYPE_SURFACE_SECURE);
|
||||
} else if (viewType == ViewType.VIEW_TYPE_TEXTURE) {
|
||||
if (!(surfaceView instanceof TextureView)) {
|
||||
surfaceView = new TextureView(context);
|
||||
viewNeedRefresh = true;
|
||||
}
|
||||
// Support opacity properly:
|
||||
((TextureView) surfaceView).setOpaque(false);
|
||||
} else {
|
||||
DebugLog.wtf(TAG, "wtf is this texture " + viewType);
|
||||
}
|
||||
if (viewNeedRefresh) {
|
||||
surfaceView.setLayoutParams(layoutParams);
|
||||
|
||||
if (layout.getChildAt(0) != null) {
|
||||
layout.removeViewAt(0);
|
||||
}
|
||||
layout.addView(surfaceView, 0, layoutParams);
|
||||
|
||||
if (this.player != null) {
|
||||
setVideoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateShutterViewVisibility() {
|
||||
shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestLayout() {
|
||||
super.requestLayout();
|
||||
post(measureAndLayout);
|
||||
}
|
||||
|
||||
// AdsLoader.AdViewProvider implementation.
|
||||
|
||||
@Override
|
||||
public ViewGroup getAdViewGroup() {
|
||||
return 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.
|
||||
*/
|
||||
public void setPlayer(ExoPlayer player) {
|
||||
if (this.player == player) {
|
||||
return;
|
||||
}
|
||||
if (this.player != null) {
|
||||
this.player.removeListener(componentListener);
|
||||
clearVideoView();
|
||||
}
|
||||
this.player = player;
|
||||
shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
|
||||
if (player != null) {
|
||||
setVideoView();
|
||||
player.addListener(componentListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the resize mode which can be of value {@link ResizeMode.Mode}
|
||||
*
|
||||
* @param resizeMode The resize mode.
|
||||
*/
|
||||
public void setResizeMode(@ResizeMode.Mode int resizeMode) {
|
||||
if (layout != null && layout.getResizeMode() != resizeMode) {
|
||||
layout.setResizeMode(resizeMode);
|
||||
post(measureAndLayout);
|
||||
}
|
||||
}
|
||||
|
||||
public void setHideShutterView(boolean hideShutterView) {
|
||||
this.hideShutterView = hideShutterView;
|
||||
updateShutterViewVisibility();
|
||||
}
|
||||
|
||||
private final Runnable measureAndLayout = () -> {
|
||||
measure(
|
||||
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
|
||||
layout(getLeft(), getTop(), getRight(), getBottom());
|
||||
};
|
||||
|
||||
private void updateForCurrentTrackSelections(Tracks tracks) {
|
||||
if (tracks == null) {
|
||||
return;
|
||||
}
|
||||
ImmutableList<Tracks.Group> groups = tracks.getGroups();
|
||||
for (Tracks.Group group: groups) {
|
||||
if (group.getType() == C.TRACK_TYPE_VIDEO && group.length > 0) {
|
||||
// get the first track of the group to identify aspect ratio
|
||||
Format format = group.getTrackFormat(0);
|
||||
|
||||
// There are weird cases when video height and width did not change with rotation so we need change aspect ration to fix it
|
||||
switch (format.rotationDegrees) {
|
||||
// update aspect ratio !
|
||||
case 90:
|
||||
case 270:
|
||||
layout.setVideoAspectRatio(format.width == 0 ? 1 : (format.height * format.pixelWidthHeightRatio) / format.width);
|
||||
default:
|
||||
layout.setVideoAspectRatio(format.height == 0 ? 1 : (format.width * format.pixelWidthHeightRatio) / format.height);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// no video tracks, in that case refresh shutterView visibility
|
||||
shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
|
||||
}
|
||||
|
||||
public void invalidateAspectRatio() {
|
||||
// Resetting aspect ratio will force layout refresh on next video size changed
|
||||
layout.invalidateAspectRatio();
|
||||
}
|
||||
|
||||
private final class ComponentListener implements Player.Listener {
|
||||
|
||||
@Override
|
||||
public void onCues(@NonNull List<Cue> cues) {
|
||||
subtitleLayout.setCues(cues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(VideoSize videoSize) {
|
||||
boolean isInitialRatio = layout.getVideoAspectRatio() == 0;
|
||||
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;
|
||||
}
|
||||
layout.setVideoAspectRatio((videoSize.width * videoSize.pixelWidthHeightRatio) / videoSize.height);
|
||||
|
||||
// React native workaround for measuring and layout on initial load.
|
||||
if (isInitialRatio) {
|
||||
post(measureAndLayout);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderedFirstFrame() {
|
||||
shutterView.setVisibility(INVISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(@NonNull Tracks tracks) {
|
||||
updateForCurrentTrackSelections(tracks);
|
||||
}
|
||||
}
|
||||
}
|
335
android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt
Normal file
335
android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt
Normal file
@ -0,0 +1,335 @@
|
||||
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)
|
||||
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"
|
||||
}
|
||||
}
|
@ -5,12 +5,19 @@ 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
|
||||
|
||||
@ -20,14 +27,22 @@ class FullScreenPlayerView(
|
||||
private val exoPlayerView: ExoPlayerView,
|
||||
private val reactExoplayerView: ReactExoplayerView,
|
||||
private val playerControlView: LegacyPlayerControlView?,
|
||||
private val onBackPressedCallback: OnBackPressedCallback
|
||||
) : Dialog(context, android.R.style.Theme_Black_NoTitleBar_Fullscreen) {
|
||||
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)
|
||||
|
||||
@ -59,10 +74,15 @@ class FullScreenPlayerView(
|
||||
|
||||
init {
|
||||
setContentView(containerView, generateDefaultLayoutParams())
|
||||
}
|
||||
override fun onBackPressed() {
|
||||
super.onBackPressed()
|
||||
onBackPressedCallback.handleOnBackPressed()
|
||||
|
||||
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() {
|
||||
@ -75,6 +95,7 @@ class FullScreenPlayerView(
|
||||
parent?.removeView(it)
|
||||
containerView.addView(it, generateDefaultLayoutParams())
|
||||
}
|
||||
updateNavigationBarVisibility()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
@ -89,6 +110,28 @@ class FullScreenPlayerView(
|
||||
}
|
||||
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 =
|
||||
@ -127,4 +170,69 @@ class FullScreenPlayerView(
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,207 @@
|
||||
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.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: (info: PictureInPictureModeChangedInfo) -> Unit = { info: PictureInPictureModeChangedInfo ->
|
||||
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 = {
|
||||
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 object : Runnable {
|
||||
override fun run() {
|
||||
context.findActivity().removeOnPictureInPictureModeChangedListener(onPictureInPictureModeChanged)
|
||||
context.findActivity().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
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,14 +1,10 @@
|
||||
package com.brentvatne.exoplayer
|
||||
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import com.brentvatne.common.api.BufferConfig
|
||||
import com.brentvatne.common.api.BufferingStrategy
|
||||
import com.brentvatne.common.api.ControlsConfig
|
||||
import com.brentvatne.common.api.ResizeMode
|
||||
import com.brentvatne.common.api.SideLoadedTextTrackList
|
||||
import com.brentvatne.common.api.Source
|
||||
import com.brentvatne.common.api.SubtitleStyle
|
||||
import com.brentvatne.common.api.ViewType
|
||||
@ -16,7 +12,6 @@ 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.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.uimanager.ThemedReactContext
|
||||
import com.facebook.react.uimanager.ViewGroupManager
|
||||
@ -28,7 +23,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
||||
private const val TAG = "ExoViewManager"
|
||||
private const val REACT_CLASS = "RCTVideo"
|
||||
private const val PROP_SRC = "src"
|
||||
private const val PROP_AD_TAG_URL = "adTagUrl"
|
||||
private const val PROP_RESIZE_MODE = "resizeMode"
|
||||
private const val PROP_REPEAT = "repeat"
|
||||
private const val PROP_SELECTED_AUDIO_TRACK = "selectedAudioTrack"
|
||||
@ -37,21 +31,18 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
||||
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_TEXT_TRACKS = "textTracks"
|
||||
private const val PROP_PAUSED = "paused"
|
||||
private const val PROP_ENTER_PICTURE_IN_PICTURE_ON_LEAVE = "enterPictureInPictureOnLeave"
|
||||
private const val PROP_MUTED = "muted"
|
||||
private const val PROP_AUDIO_OUTPUT = "audioOutput"
|
||||
private const val PROP_VOLUME = "volume"
|
||||
private const val PROP_BUFFER_CONFIG = "bufferConfig"
|
||||
private const val PROP_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK =
|
||||
"preventsDisplaySleepDuringVideoPlayback"
|
||||
private const val PROP_PROGRESS_UPDATE_INTERVAL = "progressUpdateInterval"
|
||||
private const val PROP_REPORT_BANDWIDTH = "reportBandwidth"
|
||||
private const val PROP_RATE = "rate"
|
||||
private const val PROP_MIN_LOAD_RETRY_COUNT = "minLoadRetryCount"
|
||||
private const val PROP_MAXIMUM_BIT_RATE = "maxBitRate"
|
||||
private const val PROP_PLAY_IN_BACKGROUND = "playInBackground"
|
||||
private const val PROP_CONTENT_START_TIME = "contentStartTime"
|
||||
private const val PROP_DISABLE_FOCUS = "disableFocus"
|
||||
private const val PROP_BUFFERING_STRATEGY = "bufferingStrategy"
|
||||
private const val PROP_DISABLE_DISCONNECT_ERROR = "disableDisconnectError"
|
||||
@ -79,6 +70,7 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
||||
|
||||
override fun onDropViewInstance(view: ReactExoplayerView) {
|
||||
view.cleanUpResources()
|
||||
view.exitPictureInPictureMode()
|
||||
ReactNativeVideoManager.getInstance().unregisterView(this)
|
||||
}
|
||||
|
||||
@ -92,22 +84,7 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
||||
@ReactProp(name = PROP_SRC)
|
||||
fun setSrc(videoView: ReactExoplayerView, src: ReadableMap?) {
|
||||
val context = videoView.context.applicationContext
|
||||
val source = Source.parse(src, context)
|
||||
if (source.uri == null) {
|
||||
videoView.clearSrc()
|
||||
} else {
|
||||
videoView.setSrc(source)
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_AD_TAG_URL)
|
||||
fun setAdTagUrl(videoView: ReactExoplayerView, uriString: String?) {
|
||||
if (TextUtils.isEmpty(uriString)) {
|
||||
videoView.setAdTagUrl(null)
|
||||
return
|
||||
}
|
||||
val adTagUrl = Uri.parse(uriString)
|
||||
videoView.setAdTagUrl(adTagUrl)
|
||||
videoView.setSrc(Source.parse(src, context))
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_RESIZE_MODE)
|
||||
@ -169,12 +146,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
||||
videoView.setSelectedTextTrack(typeString, value)
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_TEXT_TRACKS)
|
||||
fun setTextTracks(videoView: ReactExoplayerView, textTracks: ReadableArray?) {
|
||||
val sideLoadedTextTracks = SideLoadedTextTrackList.parse(textTracks)
|
||||
videoView.setTextTracks(sideLoadedTextTracks)
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_PAUSED, defaultBoolean = false)
|
||||
fun setPaused(videoView: ReactExoplayerView, paused: Boolean) {
|
||||
videoView.setPausedModifier(paused)
|
||||
@ -185,6 +156,11 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
||||
videoView.setMutedModifier(muted)
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_ENTER_PICTURE_IN_PICTURE_ON_LEAVE, defaultBoolean = false)
|
||||
fun setEnterPictureInPictureOnLeave(videoView: ReactExoplayerView, enterPictureInPictureOnLeave: Boolean) {
|
||||
videoView.setEnterPictureInPictureOnLeave(enterPictureInPictureOnLeave)
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_AUDIO_OUTPUT)
|
||||
fun setAudioOutput(videoView: ReactExoplayerView, audioOutput: String) {
|
||||
videoView.setAudioOutput(AudioOutput.get(audioOutput))
|
||||
@ -215,11 +191,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
||||
videoView.setMaxBitRateModifier(maxBitRate.toInt())
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_MIN_LOAD_RETRY_COUNT)
|
||||
fun setMinLoadRetryCount(videoView: ReactExoplayerView, minLoadRetryCount: Int) {
|
||||
videoView.setMinLoadRetryCountModifier(minLoadRetryCount)
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_PLAY_IN_BACKGROUND, defaultBoolean = false)
|
||||
fun setPlayInBackground(videoView: ReactExoplayerView, playInBackground: Boolean) {
|
||||
videoView.setPlayInBackground(playInBackground)
|
||||
@ -235,11 +206,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
||||
videoView.setFocusable(focusable)
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_CONTENT_START_TIME, defaultInt = -1)
|
||||
fun setContentStartTime(videoView: ReactExoplayerView, contentStartTime: Int) {
|
||||
videoView.setContentStartTime(contentStartTime)
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_BUFFERING_STRATEGY)
|
||||
fun setBufferingStrategy(videoView: ReactExoplayerView, bufferingStrategy: String) {
|
||||
val strategy = BufferingStrategy.parse(bufferingStrategy)
|
||||
@ -276,15 +242,9 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
||||
videoView.setSubtitleStyle(SubtitleStyle.parse(src))
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SHUTTER_COLOR, defaultInt = 0)
|
||||
@ReactProp(name = PROP_SHUTTER_COLOR, defaultInt = Color.BLACK)
|
||||
fun setShutterColor(videoView: ReactExoplayerView, color: Int) {
|
||||
videoView.setShutterColor(if (color == 0) Color.BLACK else color)
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_BUFFER_CONFIG)
|
||||
fun setBufferConfig(videoView: ReactExoplayerView, bufferConfig: ReadableMap?) {
|
||||
val config = BufferConfig.parse(bufferConfig)
|
||||
videoView.setBufferConfig(config)
|
||||
videoView.setShutterColor(color)
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_SHOW_NOTIFICATION_CONTROLS)
|
||||
|
@ -63,6 +63,7 @@ class VideoPlaybackService : MediaSessionService() {
|
||||
|
||||
mediaSessionsList[player] = mediaSession
|
||||
addSession(mediaSession)
|
||||
startForeground(mediaSession.player.hashCode(), buildNotification(mediaSession))
|
||||
}
|
||||
|
||||
fun unregisterPlayer(player: ExoPlayer) {
|
||||
@ -95,6 +96,10 @@ class VideoPlaybackService : MediaSessionService() {
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@ -121,7 +126,7 @@ class VideoPlaybackService : MediaSessionService() {
|
||||
}
|
||||
|
||||
private fun buildNotification(session: MediaSession): Notification {
|
||||
val returnToPlayer = Intent(this, sourceActivity).apply {
|
||||
val returnToPlayer = Intent(this, sourceActivity ?: this.javaClass).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
|
||||
@ -179,17 +184,17 @@ class VideoPlaybackService : MediaSessionService() {
|
||||
.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_notification_seek_back, "Seek Backward", seekBackwardPendingIntent) // #0
|
||||
.addAction(androidx.media3.session.R.drawable.media3_icon_rewind, "Seek Backward", seekBackwardPendingIntent) // #0
|
||||
.addAction(
|
||||
if (session.player.isPlaying) {
|
||||
androidx.media3.session.R.drawable.media3_notification_pause
|
||||
androidx.media3.session.R.drawable.media3_icon_pause
|
||||
} else {
|
||||
androidx.media3.session.R.drawable.media3_notification_play
|
||||
androidx.media3.session.R.drawable.media3_icon_play
|
||||
},
|
||||
"Toggle Play",
|
||||
togglePlayPendingIntent
|
||||
) // #1
|
||||
.addAction(androidx.media3.session.R.drawable.media3_notification_seek_forward, "Seek Forward", seekForwardPendingIntent) // #2
|
||||
.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)
|
||||
@ -209,9 +214,6 @@ class VideoPlaybackService : MediaSessionService() {
|
||||
private fun hideAllNotifications() {
|
||||
val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.cancelAll()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notificationManager.deleteNotificationChannel(NOTIFICATION_CHANEL_ID)
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanup() {
|
||||
|
@ -1,7 +1,6 @@
|
||||
package com.brentvatne.react
|
||||
|
||||
import com.brentvatne.common.toolbox.DebugLog
|
||||
import com.brentvatne.exoplayer.ReactExoplayerViewManager
|
||||
|
||||
/**
|
||||
* ReactNativeVideoManager is a singleton class which allows to manipulate / the global state of the app
|
||||
@ -23,13 +22,13 @@ class ReactNativeVideoManager : RNVPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
private var instanceList: ArrayList<ReactExoplayerViewManager> = ArrayList()
|
||||
private var instanceList: ArrayList<Any> = ArrayList()
|
||||
private var pluginList: ArrayList<RNVPlugin> = ArrayList()
|
||||
|
||||
/**
|
||||
* register a new ReactExoplayerViewManager in the managed list
|
||||
*/
|
||||
fun registerView(newInstance: ReactExoplayerViewManager) {
|
||||
fun registerView(newInstance: Any) {
|
||||
if (instanceList.size > 2) {
|
||||
DebugLog.d(TAG, "multiple Video displayed ?")
|
||||
}
|
||||
@ -39,7 +38,7 @@ class ReactNativeVideoManager : RNVPlugin {
|
||||
/**
|
||||
* unregister existing ReactExoplayerViewManager in the managed list
|
||||
*/
|
||||
fun unregisterView(newInstance: ReactExoplayerViewManager) {
|
||||
fun unregisterView(newInstance: Any) {
|
||||
instanceList.remove(newInstance)
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,12 @@
|
||||
package com.brentvatne.react
|
||||
|
||||
import com.brentvatne.common.api.Source
|
||||
import com.brentvatne.exoplayer.ReactExoplayerView
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||
import com.facebook.react.bridge.ReactMethod
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.UiThreadUtil
|
||||
import com.facebook.react.uimanager.UIManagerHelper
|
||||
import com.facebook.react.uimanager.common.UIManagerType
|
||||
@ -42,6 +44,7 @@ class VideoManagerModule(reactContext: ReactApplicationContext?) : ReactContextB
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
@Suppress("UNUSED_PARAMETER") // codegen compatibility
|
||||
fun seekCmd(reactTag: Int, time: Float, tolerance: Float) {
|
||||
performOnPlayerView(reactTag) {
|
||||
it?.seekTo((time * 1000f).roundToInt().toLong())
|
||||
@ -62,6 +65,27 @@ class VideoManagerModule(reactContext: ReactApplicationContext?) : ReactContextB
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun enterPictureInPictureCmd(reactTag: Int) {
|
||||
performOnPlayerView(reactTag) {
|
||||
it?.enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun exitPictureInPictureCmd(reactTag: Int) {
|
||||
performOnPlayerView(reactTag) {
|
||||
it?.exitPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun setSourceCmd(reactTag: Int, source: ReadableMap?) {
|
||||
performOnPlayerView(reactTag) {
|
||||
it?.setSrc(Source.parse(source, reactApplicationContext))
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun getCurrentPosition(reactTag: Int, promise: Promise) {
|
||||
performOnPlayerView(reactTag) {
|
||||
|
@ -0,0 +1,72 @@
|
||||
package com.brentvatne.receiver
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.brentvatne.exoplayer.ReactExoplayerView
|
||||
import com.facebook.react.uimanager.ThemedReactContext
|
||||
|
||||
class PictureInPictureReceiver(private val view: ReactExoplayerView, private val context: ThemedReactContext) : BroadcastReceiver() {
|
||||
|
||||
companion object {
|
||||
const val ACTION_MEDIA_CONTROL = "rnv_media_control"
|
||||
const val EXTRA_CONTROL_TYPE = "rnv_control_type"
|
||||
|
||||
// The request code for play action PendingIntent.
|
||||
const val REQUEST_PLAY = 1
|
||||
|
||||
// The request code for pause action PendingIntent.
|
||||
const val REQUEST_PAUSE = 2
|
||||
|
||||
// The intent extra value for play action.
|
||||
const val CONTROL_TYPE_PLAY = 1
|
||||
|
||||
// The intent extra value for pause action.
|
||||
const val CONTROL_TYPE_PAUSE = 2
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
intent ?: return
|
||||
if (intent.action == ACTION_MEDIA_CONTROL) {
|
||||
when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) {
|
||||
CONTROL_TYPE_PLAY -> view.setPausedModifier(false)
|
||||
CONTROL_TYPE_PAUSE -> view.setPausedModifier(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setListener() {
|
||||
ContextCompat.registerReceiver(context, this, IntentFilter(ACTION_MEDIA_CONTROL), ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
}
|
||||
|
||||
fun removeListener() {
|
||||
try {
|
||||
context.unregisterReceiver(this)
|
||||
} catch (e: Exception) {
|
||||
// ignore if already unregistered
|
||||
}
|
||||
}
|
||||
|
||||
fun getPipActionIntent(isPaused: Boolean): PendingIntent {
|
||||
val requestCode = if (isPaused) REQUEST_PLAY else REQUEST_PAUSE
|
||||
val controlType = if (isPaused) CONTROL_TYPE_PLAY else CONTROL_TYPE_PAUSE
|
||||
val flag =
|
||||
if (Build.VERSION.SDK_INT >=
|
||||
Build.VERSION_CODES.M
|
||||
) {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
val intent = Intent(ACTION_MEDIA_CONTROL).putExtra(
|
||||
EXTRA_CONTROL_TYPE,
|
||||
controlType
|
||||
)
|
||||
intent.setPackage(context.packageName)
|
||||
return PendingIntent.getBroadcast(context, requestCode, intent, flag)
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.google.ads.interactivemedia.v3.api;
|
||||
|
||||
public abstract class ImaSdkFactory {
|
||||
private static ImaSdkFactory instance;
|
||||
|
||||
public abstract ImaSdkSettings createImaSdkSettings();
|
||||
|
||||
public static ImaSdkFactory getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new ConcreteImaSdkFactory();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
class ConcreteImaSdkFactory extends ImaSdkFactory {
|
||||
|
||||
@Override
|
||||
public ImaSdkSettings createImaSdkSettings() {
|
||||
return new ConcreteImaSdkSettings();
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package com.google.ads.interactivemedia.v3.api;
|
||||
|
||||
import androidx.annotation.InspectableProperty;
|
||||
|
||||
public abstract class ImaSdkSettings {
|
||||
public abstract String getLanguage();
|
||||
public abstract void setLanguage(String language);
|
||||
}
|
||||
|
||||
// Concrete Implementation
|
||||
class ConcreteImaSdkSettings extends ImaSdkSettings {
|
||||
|
||||
private String language;
|
||||
|
||||
@Override
|
||||
public String getLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLanguage(String language) {
|
||||
this.language = language;
|
||||
}
|
||||
}
|
6
android/src/main/res/drawable/circle.xml
Normal file
6
android/src/main/res/drawable/circle.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/red"/>
|
||||
<size android:width="10dp" android:height="10dp"/>
|
||||
</shape>
|
@ -1,17 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_height="match_parent"
|
||||
android:layoutDirection="ltr"
|
||||
android:background="@color/midnight_black"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="@dimen/live_wrapper_margin_top"
|
||||
android:id="@+id/exo_live_container">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/exo_live_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="@dimen/position_duration_horizontal_padding"
|
||||
android:src="@drawable/circle" />
|
||||
|
||||
<TextView android:id="@+id/exo_live_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/position_duration_text_size"
|
||||
android:textStyle="bold"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/white"/>
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="@dimen/controller_wrapper_padding_top"
|
||||
android:layout_gravity="bottom"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton android:id="@+id/exo_prev"
|
||||
@ -70,6 +101,13 @@
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/silver_gray"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/exo_settings"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/ExoStyledControls.Button.Bottom.Settings"
|
||||
/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/exo_fullscreen"
|
||||
style="@style/ExoMediaButton.FullScreen"
|
||||
|
@ -2,4 +2,6 @@
|
||||
<resources>
|
||||
<color name="silver_gray">#FFBEBEBE</color>
|
||||
<color name="midnight_black">#CC000000</color>
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="red">#FF0000</color>
|
||||
</resources>
|
@ -3,6 +3,7 @@
|
||||
<!-- margin & padding-->
|
||||
<dimen name="controller_wrapper_padding_top">4dp</dimen>
|
||||
<dimen name="seekBar_wrapper_margin_top">4dp</dimen>
|
||||
<dimen name="live_wrapper_margin_top">12dp</dimen>
|
||||
<dimen name="position_duration_horizontal_padding">4dp</dimen>
|
||||
<dimen name="full_screen_margin">4dp</dimen>
|
||||
|
||||
|
@ -16,4 +16,10 @@
|
||||
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
|
||||
|
||||
<string name="error_drm_unknown">An unknown DRM error occurred</string>
|
||||
|
||||
<string name="settings">Settings</string>
|
||||
|
||||
<string name="playback_speed">Playback Speed</string>
|
||||
|
||||
<string name="select_playback_speed">Select Playback Speed</string>
|
||||
</resources>
|
||||
|
BIN
docs/bun.lockb
BIN
docs/bun.lockb
Binary file not shown.
@ -3,6 +3,6 @@
|
||||
}
|
||||
|
||||
.spanStyle {
|
||||
font-family: 'Orbitron';
|
||||
font-family: var(--font-orbitron);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
62
docs/components/TWGBadge/TWGBadge.module.css
Normal file
62
docs/components/TWGBadge/TWGBadge.module.css
Normal file
@ -0,0 +1,62 @@
|
||||
.extraContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 0.5rem;
|
||||
text-align: center;
|
||||
background-color: #171717;
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.extraText {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.extraButton {
|
||||
width: 100%;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 500;
|
||||
background-color: #f9d85b;
|
||||
transition: transform 0.3s ease, background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.extraButton:hover {
|
||||
transform: scale(1.05);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
:is(html[class~=dark]) .extraContainer {
|
||||
background-color: #87ccef;
|
||||
}
|
||||
|
||||
:is(html[class~=dark]) .extraText {
|
||||
color: #171717;
|
||||
}
|
||||
|
||||
:is(html[class~=dark]) .extraButton {
|
||||
background-color: #171717;
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.visibleOnLarge {
|
||||
display: inherit;
|
||||
}
|
||||
.visibleOnSmall {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1279px) {
|
||||
.visibleOnLarge {
|
||||
display: none;
|
||||
}
|
||||
.visibleOnSmall {
|
||||
display: flex;
|
||||
}
|
||||
}
|
27
docs/components/TWGBadge/TWGBadge.tsx
Normal file
27
docs/components/TWGBadge/TWGBadge.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import styles from './TWGBadge.module.css';
|
||||
|
||||
interface TWGBadgeProps {
|
||||
visibleOnLarge?: boolean;
|
||||
}
|
||||
|
||||
const TWGBadge = ({visibleOnLarge}: TWGBadgeProps) => {
|
||||
const visibilityClass = visibleOnLarge
|
||||
? styles.visibleOnLarge
|
||||
: styles.visibleOnSmall;
|
||||
|
||||
return (
|
||||
<div className={[styles.extraContainer, visibilityClass].join(' ')}>
|
||||
<span className={styles.extraText}>We are TheWidlarzGroup</span>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs#Contact"
|
||||
className={styles.extraButton}
|
||||
rel="noreferrer">
|
||||
Premium support →
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TWGBadge;
|
7
docs/font.ts
Normal file
7
docs/font.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import {Orbitron} from 'next/font/google';
|
||||
|
||||
export const orbitron = Orbitron({
|
||||
display: 'swap',
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '900'],
|
||||
});
|
2
docs/next-env.d.ts
vendored
2
docs/next-env.d.ts
vendored
@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
|
||||
|
@ -8,7 +8,7 @@
|
||||
"build": "bun next build"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^13.5.4",
|
||||
"next": "14.2.20",
|
||||
"nextra": "^2.13.2",
|
||||
"nextra-theme-docs": "^2.13.2",
|
||||
"react": "^18.2.0",
|
||||
@ -18,4 +18,4 @@
|
||||
"devDependencies": {
|
||||
"bun-types": "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
docs/pages/_app.mdx
Normal file
14
docs/pages/_app.mdx
Normal file
@ -0,0 +1,14 @@
|
||||
import {orbitron} from '../font';
|
||||
|
||||
export default function Nextra({Component, pageProps}) {
|
||||
return (
|
||||
<>
|
||||
<style jsx global>{`
|
||||
:root {
|
||||
--font-orbitron: ${orbitron.style.fontFamily};
|
||||
}
|
||||
`}</style>
|
||||
<Component {...pageProps} />
|
||||
</>
|
||||
);
|
||||
}
|
@ -17,5 +17,19 @@
|
||||
"type": "separator",
|
||||
"title": ""
|
||||
},
|
||||
"projects": "Useful projects"
|
||||
}
|
||||
"example_apps": {
|
||||
"title": "Example Apps",
|
||||
"newWindow": true,
|
||||
"href": "https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples"
|
||||
},
|
||||
"projects": "Useful projects",
|
||||
"separator_enterprise": {
|
||||
"type": "separator",
|
||||
"title": ""
|
||||
},
|
||||
"enterprise_support": {
|
||||
"title": "Enterprise Support",
|
||||
"newWindow": true,
|
||||
"href": "https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs#Contact"
|
||||
}
|
||||
}
|
||||
|
@ -23,3 +23,16 @@ Example:
|
||||
onReceiveAdEvent={event => console.log(event)}
|
||||
...
|
||||
```
|
||||
|
||||
### Localization
|
||||
To change the language of the IMA SDK, you need to pass `adLanguage` prop to `Video` component. List of supported languages, you can find [here](https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/localization#locale-codes)
|
||||
|
||||
By default, ios will use system language and android will use `en`
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
...
|
||||
adLanguage="fr"
|
||||
...
|
||||
```
|
||||
|
@ -2,7 +2,10 @@ import PlatformsList from '../../components/PlatformsList/PlatformsList.tsx';
|
||||
|
||||
# DRM
|
||||
|
||||
> **Note:** DRM is not supported on visionOS yet.
|
||||
## DRM Example
|
||||
|
||||
We have available example for DRM usage in the [example app](https://github.com/TheWidlarzGroup/react-native-video/blob/master/examples/bare/src/DRMExample.tsx).
|
||||
To get token needed for DRM playback you can go to [our site](https://www.thewidlarzgroup.com/services/free-drm-token-generator-for-video?utm_source=drm&utm_medium=docs) and get it.
|
||||
|
||||
## Provide DRM data (only tested with http/https assets)
|
||||
|
||||
@ -13,7 +16,7 @@ DRM object allows this members:
|
||||
|
||||
### `base64Certificate`
|
||||
|
||||
<PlatformsList types={['iOS']} />
|
||||
<PlatformsList types={['iOS', 'visionOS']} />
|
||||
|
||||
Type: bool\
|
||||
Default: false
|
||||
@ -22,7 +25,7 @@ Whether or not the certificate url returns it on base64.
|
||||
|
||||
### `certificateUrl`
|
||||
|
||||
<PlatformsList types={['iOS']} />
|
||||
<PlatformsList types={['iOS', 'visionOS']} />
|
||||
|
||||
Type: string\
|
||||
Default: undefined
|
||||
@ -31,7 +34,7 @@ URL to fetch a valid certificate for FairPlay.
|
||||
|
||||
### `getLicense`
|
||||
|
||||
<PlatformsList types={['iOS']} />
|
||||
<PlatformsList types={['iOS', 'visionOS']} />
|
||||
|
||||
Type: function\
|
||||
Default: undefined
|
||||
@ -44,8 +47,8 @@ your `contentId` + the provided certificate via `objc [loadingRequest streamingC
|
||||
contentIdentifier:contentIdData options:nil error:&spcError]; `
|
||||
|
||||
Also, you will receive following parameter of getLicense:
|
||||
* `contentId` contentId if passed to `drm` object or loadingRequest.request.url?.host
|
||||
* `loadedLicenseUrl` URL defined as `loadingRequest.request.URL.absoluteString`, this url starts with `skd://` or `clearkey://`
|
||||
* `contentId` contentId if passed to `drm` object or loadingRequest.request.url?.host
|
||||
* `loadedLicenseUrl` URL defined as `loadingRequest.request.URL.absoluteString`, this url starts with `skd://` or `clearkey://`
|
||||
* `licenseServer` prop if prop is passed to `drm` object.
|
||||
* `spcString` the SPC used to validate playback with drm server
|
||||
|
||||
@ -79,7 +82,7 @@ getLicense: (spcString, contentId, licenseUrl, loadedLicenseUrl) => {
|
||||
|
||||
### `contentId`
|
||||
|
||||
<PlatformsList types={['iOS']} />
|
||||
<PlatformsList types={['iOS', 'visionOS']} />
|
||||
|
||||
Type: string\
|
||||
Default: undefined
|
||||
@ -88,7 +91,7 @@ Specify the content id of the stream, otherwise it will take the host value from
|
||||
|
||||
### `headers`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||
|
||||
Type: Object\
|
||||
Default: undefined
|
||||
@ -112,7 +115,7 @@ drm={{
|
||||
|
||||
### `licenseServer`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||
|
||||
Type: string\
|
||||
Default: false
|
||||
@ -137,6 +140,20 @@ You can specify the DRM type, either by string or using the exported DRMType enu
|
||||
Valid values are, for Android: DRMType.WIDEVINE / DRMType.PLAYREADY / DRMType.CLEARKEY.
|
||||
for iOS: DRMType.FAIRPLAY
|
||||
|
||||
### `localSourceEncryptionKeyScheme`
|
||||
|
||||
<PlatformsList types={['iOS', 'visionOS']} />
|
||||
|
||||
Set the url scheme for stream encryption key for local assets
|
||||
|
||||
Type: String
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
localSourceEncryptionKeyScheme="my-offline-key"
|
||||
```
|
||||
|
||||
## Common Usage Scenarios
|
||||
|
||||
### Send cookies to license server
|
||||
|
@ -67,7 +67,7 @@ Example:
|
||||
|
||||
### `onBandwidthUpdate`
|
||||
|
||||
<PlatformsList types={['Android']} />
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
|
||||
Callback function that is called when the available bandwidth changes.
|
||||
|
||||
@ -103,7 +103,7 @@ Note: On Android, you must set the [reportBandwidth](#reportbandwidth) prop to e
|
||||
|
||||
### `onBuffer`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||
|
||||
Callback function that is called when the player buffers.
|
||||
|
||||
@ -219,16 +219,20 @@ Payload: none
|
||||
|
||||
Callback function that is called when the media is loaded and ready to play.
|
||||
|
||||
|
||||
NOTE: tracks (`audioTracks`, `textTracks` & `videoTracks`) are not available on the web.
|
||||
|
||||
Payload:
|
||||
|
||||
| Property | Type | Description |
|
||||
| ----------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
|-------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| currentTime | number | Time in seconds where the media will start |
|
||||
| duration | number | Length of the media in seconds |
|
||||
| naturalSize | object | Properties:<br/> _ width - Width in pixels that the video was encoded at<br/> _ height - Height in pixels that the video was encoded at<br/> \* orientation - "portrait", "landscape" or "square" |
|
||||
| audioTracks | array | An array of audio track info objects with the following properties:<br/> _ index - Index number<br/> _ title - Description of the track<br/> _ language - 2 letter [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) or 3 letter [ISO639-2](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) language code<br/> _ type - Mime type of track |
|
||||
| textTracks | array | An array of text track info objects with the following properties:<br/> _ index - Index number<br/> _ title - Description of the track<br/> _ language - 2 letter [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) or 3 letter [ISO 639-2](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) language code<br/> _ type - Mime type of track |
|
||||
| videoTracks | array | An array of video track info objects with the following properties:<br/> _ trackId - ID for the track<br/> _ bitrate - Bit rate in bits per second<br/> _ codecs - Comma separated list of codecs<br/> _ height - Height of the video<br/> \* width - Width of the video |
|
||||
| trackId | string | Provide key information about the video track, typically including: `Resolution`, `Bitrate`. |
|
||||
|
||||
Example:
|
||||
|
||||
@ -260,7 +264,8 @@ Example:
|
||||
{ index: 0, bitrate: 3987904, codecs: "avc1.640028", height: 720, trackId: "f1-v1-x3", width: 1280 },
|
||||
{ index: 1, bitrate: 7981888, codecs: "avc1.640028", height: 1080, trackId: "f2-v1-x3", width: 1920 },
|
||||
{ index: 2, bitrate: 1994979, codecs: "avc1.4d401f", height: 480, trackId: "f3-v1-x3", width: 848 }
|
||||
]
|
||||
],
|
||||
trackId: "720p 2400kbps"
|
||||
}
|
||||
```
|
||||
|
||||
@ -290,7 +295,7 @@ Example:
|
||||
|
||||
### `onPlaybackStateChanged`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
|
||||
|
||||
Callback function that is called when the playback state changes.
|
||||
|
||||
@ -312,7 +317,7 @@ Example:
|
||||
|
||||
### `onPictureInPictureStatusChanged`
|
||||
|
||||
<PlatformsList types={['iOS']} />
|
||||
<PlatformsList types={['iOS', 'Android', 'web']} />
|
||||
|
||||
Callback function that is called when picture in picture becomes active or inactive.
|
||||
|
||||
@ -461,7 +466,7 @@ Payload: none
|
||||
|
||||
### `onSeek`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'Windows UWP']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'Windows UWP', 'web']} />
|
||||
|
||||
Callback function that is called when a seek completes.
|
||||
|
||||
@ -602,7 +607,7 @@ Example:
|
||||
|
||||
### `onVolumeChange`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
|
||||
|
||||
Callback function that is called when the volume of player changes.
|
||||
|
||||
|
@ -6,7 +6,7 @@ This page shows the list of available methods
|
||||
|
||||
### `dismissFullscreenPlayer`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||
|
||||
`dismissFullscreenPlayer(): Promise<void>`
|
||||
|
||||
@ -17,7 +17,7 @@ Take the player out of fullscreen mode.
|
||||
|
||||
### `pause`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||
|
||||
`pause(): Promise<void>`
|
||||
|
||||
@ -25,7 +25,7 @@ Pause the video.
|
||||
|
||||
### `presentFullscreenPlayer`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||
|
||||
`presentFullscreenPlayer(): Promise<void>`
|
||||
|
||||
@ -40,7 +40,7 @@ On Android, this puts the navigation controls in fullscreen mode. It is not a co
|
||||
|
||||
### `resume`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||
|
||||
`resume(): Promise<void>`
|
||||
|
||||
@ -78,6 +78,56 @@ Future:
|
||||
- Will support more formats in the future through options
|
||||
- Will support custom directory and file name through options
|
||||
|
||||
### `enterPictureInPicture`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||
|
||||
`enterPictureInPicture()`
|
||||
|
||||
To use this feature on Android with Expo, you must set 'enableAndroidPictureInPicture' true within expo plugin config (app.json)
|
||||
|
||||
```json
|
||||
"plugins": [
|
||||
[
|
||||
"react-native-video",
|
||||
{
|
||||
"enableAndroidPictureInPicture": true,
|
||||
}
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
To use this feature on Android with Bare React Native, you must:
|
||||
|
||||
- [Declare PiP support](https://developer.android.com/develop/ui/views/picture-in-picture#declaring) in your AndroidManifest.xml
|
||||
- setting `android:supportsPictureInPicture` to `true`
|
||||
|
||||
```xml
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
...
|
||||
android:supportsPictureInPicture="true">
|
||||
```
|
||||
|
||||
NOTE: Foreground picture in picture is not supported on Android due to limitations of react native (Single Activity App). So, If you call `enterPictureInPicture`, application will switch to background on Android.
|
||||
NOTE: Video ads cannot start when you are using the PIP on iOS (more info available at [Google IMA SDK Docs](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/picture_in_picture?hl=en#starting_ads)). If you are using custom controls, you must hide your PIP button when you receive the `STARTED` event from `onReceiveAdEvent` and show it again when you receive the `ALL_ADS_COMPLETED` event.
|
||||
|
||||
### `exitPictureInPicture`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||
|
||||
`exitPictureInPicture()`
|
||||
|
||||
Exits the active picture in picture; if it is not active, the function call is ignored.
|
||||
|
||||
### `restoreUserInterfaceForPictureInPictureStopCompleted`
|
||||
|
||||
<PlatformsList types={['iOS']} />
|
||||
|
||||
`restoreUserInterfaceForPictureInPictureStopCompleted(restored)`
|
||||
|
||||
This function corresponds to the completion handler in Apple's [restoreUserInterfaceForPictureInPictureStop](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). IMPORTANT: This function must be called after `onRestoreUserInterfaceForPictureInPictureStop` is called.
|
||||
|
||||
### `seek`
|
||||
|
||||
<PlatformsList types={['All']} />
|
||||
@ -100,7 +150,7 @@ tolerance is the max distance in milliseconds from the seconds position that's a
|
||||
|
||||
### `setVolume`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||
|
||||
`setVolume(value): Promise<void>`
|
||||
|
||||
@ -108,17 +158,27 @@ This function will change the volume exactly like [volume](./props#volume) prope
|
||||
|
||||
### `getCurrentPosition`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||
|
||||
`getCurrentPosition(): Promise<number>`
|
||||
|
||||
This function retrieves and returns the precise current position of the video playback, measured in seconds.
|
||||
This function will throw an error if player is not initialized.
|
||||
|
||||
### `setFullScreen`
|
||||
|
||||
### `setSource`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
|
||||
`setSource(source: ReactVideoSource): Promise<void>`
|
||||
|
||||
This function will change the source exactly like [source](./props#source) property.
|
||||
Changing source with this function will overide source provided as props.
|
||||
|
||||
### `setFullScreen`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||
|
||||
`setFullScreen(fullscreen): Promise<void>`
|
||||
|
||||
If you set it to `true`, the player enters fullscreen mode. If you set it to `false`, the player exits fullscreen mode.
|
||||
@ -127,6 +187,13 @@ On iOS, this displays the video in a fullscreen view controller with controls.
|
||||
|
||||
On Android, this puts the navigation controls in fullscreen mode. It is not a complete fullscreen implementation, so you will still need to apply a style that makes the width and height match your screen dimensions to get a fullscreen video.
|
||||
|
||||
### `nativeHtmlVideoRef`
|
||||
|
||||
<PlatformsList types={['web']} />
|
||||
|
||||
A ref to the underlying html video element. This can be used if you need to integrate a 3d party, web only video library (like hls.js, shaka, video.js...).
|
||||
|
||||
|
||||
### Example Usage
|
||||
|
||||
```tsx
|
||||
@ -141,9 +208,9 @@ const someCoolFunctions = async () => {
|
||||
videoRef.current.presentFullscreenPlayer();
|
||||
videoRef.current.dismissFullscreenPlayer();
|
||||
|
||||
// pause or play the video
|
||||
videoRef.current.play();
|
||||
// pause or resume the video
|
||||
videoRef.current.pause();
|
||||
videoRef.current.resume();
|
||||
|
||||
// save video to your Photos with current filter prop
|
||||
const response = await videoRef.current.save();
|
||||
@ -178,7 +245,7 @@ Possible values are:
|
||||
|
||||
### `isCodecSupported`
|
||||
|
||||
<PlatformsList types={['Android']} />
|
||||
<PlatformsList types={['Android', 'web']} />
|
||||
|
||||
Indicates whether the provided codec is supported level supported by device.
|
||||
|
||||
|
@ -8,6 +8,9 @@ This page shows the list of available properties to configure player
|
||||
|
||||
### `adTagUrl`
|
||||
|
||||
> [!WARNING]
|
||||
> Deprecated, use source.ad.adTagUrl instead
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
|
||||
Sets the VAST uri to play AVOD ads.
|
||||
@ -49,6 +52,9 @@ A Boolean value that indicates whether the player should automatically delay pla
|
||||
|
||||
### `bufferConfig`
|
||||
|
||||
> [!WARNING]
|
||||
> Deprecated, use source.bufferConfig instead
|
||||
|
||||
<PlatformsList types={['Android']} />
|
||||
|
||||
Adjust the buffer settings. This prop takes an object with one or more of the properties listed below.
|
||||
@ -128,15 +134,14 @@ When playing an HLS live stream with a `EXT-X-PROGRAM-DATE-TIME` tag configured,
|
||||
|
||||
### `controls`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
|
||||
|
||||
Determines whether to show player controls.
|
||||
|
||||
- **false (default)** - Don't show player controls
|
||||
- **true** - Show player controls
|
||||
|
||||
Note on iOS, controls are always shown when in fullscreen mode.
|
||||
Note on Android, native controls are available by default.
|
||||
Controls are always shown in fullscreen mode, even when `controls={false}`.
|
||||
If needed, you can also add your controls or use a package like [react-native-video-controls](https://github.com/itsnubix/react-native-video-controls) or [react-native-media-console](https://github.com/criszz77/react-native-media-console), see [Useful Side Project](/projects).
|
||||
|
||||
### `controlsStyles`
|
||||
@ -145,25 +150,53 @@ If needed, you can also add your controls or use a package like [react-native-vi
|
||||
|
||||
Adjust the control styles. This prop is need only if `controls={true}` and is an object. See the list of prop supported below.
|
||||
|
||||
| Property | Type | Description |
|
||||
|-----------------|---------|-----------------------------------------------------------------------------------------|
|
||||
| hideSeekBar | boolean | The default value is `false`, allowing you to hide the seek bar for live broadcasts. |
|
||||
| seekIncrementMS | number | The default value is `10000`. You can change the value to increment forward and rewind. |
|
||||
| Property | Type | Description |
|
||||
|-------------------------------------|---------|---------------------------------------------------------------------------------------------|
|
||||
| hidePosition | boolean | Hides the position indicator. Default is `false`. |
|
||||
| hidePlayPause | boolean | Hides the play/pause button. Default is `false`. |
|
||||
| hideForward | boolean | Hides the forward button. Default is `false`. |
|
||||
| hideRewind | boolean | Hides the rewind button. Default is `false`. |
|
||||
| hideNext | boolean | Hides the next button. Default is `false`. |
|
||||
| hidePrevious | boolean | Hides the previous button. Default is `false`. |
|
||||
| hideFullscreen | boolean | Hides the fullscreen button. Default is `false`. |
|
||||
| hideSeekBar | boolean | The default value is `false`, allowing you to hide the seek bar for live broadcasts. |
|
||||
| hideDuration | boolean | The default value is `false`, allowing you to hide the duration. |
|
||||
| hideNavigationBarOnFullScreenMode | boolean | The default value is `true`, allowing you to hide the navigation bar on full-screen mode. |
|
||||
| hideNotificationBarOnFullScreenMode | boolean | The default value is `true`, allowing you to hide the notification bar on full-screen mode. |
|
||||
| hideSettingButton | boolean | The default value is `true`, allowing you to hide the setting button. |
|
||||
| seekIncrementMS | number | The default value is `10000`. You can change the value to increment forward and rewind. |
|
||||
| liveLabel | string | Allowing you to set a label for live video. |
|
||||
|
||||
Example with default values:
|
||||
|
||||
```javascript
|
||||
controlsStyles={{
|
||||
hidePosition: false,
|
||||
hidePlayPause: false,
|
||||
hideForward: false,
|
||||
hideRewind: false,
|
||||
hideNext: false,
|
||||
hidePrevious: false,
|
||||
hideFullscreen: false,
|
||||
hideSeekBar: false,
|
||||
hideDuration: false,
|
||||
hideNavigationBarOnFullScreenMode: true,
|
||||
hideNotificationBarOnFullScreenMode: true,
|
||||
hideSettingButton: true,
|
||||
seekIncrementMS: 10000,
|
||||
liveLabel: "LIVE"
|
||||
}}
|
||||
```
|
||||
|
||||
### `contentStartTime`
|
||||
|
||||
> [!WARNING]
|
||||
> Deprecated, use source.contentStartTime instead
|
||||
|
||||
<PlatformsList types={['Android']} />
|
||||
|
||||
The start time in ms for SSAI content. This determines at what time to load the video info like resolutions. Use this only when you have SSAI stream where ads resolution is not the same as content resolution.
|
||||
Note: This feature only works on DASH streams
|
||||
|
||||
### `debug`
|
||||
|
||||
@ -219,6 +252,42 @@ To setup DRM please follow [this guide](/component/drm)
|
||||
|
||||
> ⚠️ DRM is not supported on visionOS yet
|
||||
|
||||
### `enterPictureInPictureOnLeave`
|
||||
|
||||
<PlatformsList types={['iOS', 'Android']} />
|
||||
|
||||
Determine whether to play media as a picture in picture when the user goes to the background.
|
||||
|
||||
- **false (default)** - Don't not play as picture in picture
|
||||
- **true** - Play the media as picture in picture
|
||||
|
||||
To use this feature on Android with Expo, you must set 'enableAndroidPictureInPicture' true within expo plugin config (app.json)
|
||||
|
||||
```json
|
||||
"plugins": [
|
||||
[
|
||||
"react-native-video",
|
||||
{
|
||||
"enableAndroidPictureInPicture": true,
|
||||
}
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
To use this feature on Android with Bare React Native, you must:
|
||||
|
||||
- [Declare PiP support](https://developer.android.com/develop/ui/views/picture-in-picture#declaring) in your AndroidManifest.xml
|
||||
- setting `android:supportsPictureInPicture` to `true`
|
||||
|
||||
```xml
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
...
|
||||
android:supportsPictureInPicture="true">
|
||||
```
|
||||
|
||||
NOTE: Video ads cannot start when you are using the PIP on iOS (more info available at [Google IMA SDK Docs](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/picture_in_picture?hl=en#starting_ads)). If you are using custom controls, you must hide your PIP button when you receive the `STARTED` event from `onReceiveAdEvent` and show it again when you receive the `ALL_ADS_COMPLETED` event.
|
||||
|
||||
### `filter`
|
||||
|
||||
<PlatformsList types={['iOS', 'visionOS']} />
|
||||
@ -270,7 +339,7 @@ Whether this video view should be focusable with a non-touch input device, eg. r
|
||||
|
||||
### `fullscreen`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
|
||||
|
||||
Controls whether the player enters fullscreen on play.
|
||||
See [presentFullscreenPlayer](#presentfullscreenplayer) for details.
|
||||
@ -286,7 +355,7 @@ If a preferred [fullscreenOrientation](#fullscreenorientation) is set, causes th
|
||||
|
||||
### `fullscreenOrientation`
|
||||
|
||||
<PlatformsList types={['iOS', 'visionOS']} />
|
||||
<PlatformsList types={['iOS', 'visionOS', 'web']} />
|
||||
|
||||
- **all (default)** -
|
||||
- **landscape**
|
||||
@ -330,19 +399,6 @@ Controls the iOS silent switch behavior
|
||||
- **"ignore"** - Play audio even if the silent switch is set
|
||||
- **"obey"** - Don't play audio if the silent switch is set
|
||||
|
||||
### `localSourceEncryptionKeyScheme`
|
||||
|
||||
<PlatformsList types={['iOS']} />
|
||||
|
||||
Set the url scheme for stream encryption key for local assets
|
||||
|
||||
Type: String
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
localSourceEncryptionKeyScheme="my-offline-key"
|
||||
```
|
||||
|
||||
### `maxBitRate`
|
||||
|
||||
@ -352,6 +408,9 @@ Sets the desired limit, in bits per second, of network bandwidth consumption whe
|
||||
|
||||
Default: 0. Don't limit the maxBitRate.
|
||||
|
||||
Note: This property can interact with selectedVideoTrack.
|
||||
To use `maxBitrate`, selectedVideoTrack shall be undefined or `{type: SelectedVideoTrackType.AUTO}`.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
@ -359,6 +418,8 @@ maxBitRate={2000000} // 2 megabits
|
||||
```
|
||||
|
||||
### `minLoadRetryCount`
|
||||
> [!WARNING]
|
||||
> deprecated, use `source.minLoadRetryCount` key instead
|
||||
|
||||
<PlatformsList types={['Android']} />
|
||||
|
||||
@ -400,17 +461,6 @@ Controls whether the media is paused
|
||||
- **false (default)** - Don't pause the media
|
||||
- **true** - Pause the media
|
||||
|
||||
### `pictureInPicture`
|
||||
|
||||
<PlatformsList types={['iOS']} />
|
||||
|
||||
Determine whether the media should played as picture in picture.
|
||||
|
||||
- **false (default)** - Don't not play as picture in picture
|
||||
- **true** - Play the media as picture in picture
|
||||
|
||||
NOTE: Video ads cannot start when you are using the PIP on iOS (more info available at [Google IMA SDK Docs](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/picture_in_picture?hl=en#starting_ads)). If you are using custom controls, you must hide your PIP button when you receive the `STARTED` event from `onReceiveAdEvent` and show it again when you receive the `ALL_ADS_COMPLETED` event.
|
||||
|
||||
### `playInBackground`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||
@ -506,14 +556,29 @@ Speed at which the media should play.
|
||||
|
||||
<PlatformsList types={['All']} />
|
||||
|
||||
Allows you to create custom components to display while the video is loading. If `renderLoader` is provided, `poster` and `posterResizeMode` will be ignored.
|
||||
Allows you to create custom components to display while the video is loading.
|
||||
If `renderLoader` is provided, `poster` and `posterResizeMode` will be ignored.
|
||||
renderLoader is either a component or a function returning a component.
|
||||
It is recommended to use the function for optimization matter.
|
||||
|
||||
`renderLoader` function be called with parameters of type `ReactVideoRenderLoaderProps` to be able to adapt loader
|
||||
|
||||
```typescript
|
||||
interface ReactVideoRenderLoaderProps {
|
||||
source?: ReactVideoSource; /// source of the video
|
||||
style?: StyleProp<ImageStyle>; /// style to apply
|
||||
resizeMode?: EnumValues<VideoResizeMode>; /// resizeMode provided to the video component
|
||||
}
|
||||
````
|
||||
|
||||
Sample:
|
||||
|
||||
```javascript
|
||||
<Video>
|
||||
renderLoader={
|
||||
renderLoader={() => (
|
||||
<View>
|
||||
<Text>Custom Loader</Text>
|
||||
</View>
|
||||
</View>)
|
||||
}
|
||||
</Video>
|
||||
````
|
||||
@ -672,6 +737,8 @@ The docs for this prop are incomplete and will be updated as each option is inve
|
||||
|
||||
> ⚠️ on iOS, you file name must not contain spaces eg. `my video.mp4` will not work, use `my-video.mp4` instead
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS', 'Windows UWP']} />
|
||||
|
||||
Example:
|
||||
|
||||
Pass directly the asset to play (deprecated)
|
||||
@ -762,7 +829,7 @@ The following other types are supported on some platforms, but aren't fully docu
|
||||
|
||||
#### Using DRM content
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS', 'tvOS']} />
|
||||
|
||||
To setup DRM please follow [this guide](/component/drm)
|
||||
|
||||
@ -780,12 +847,10 @@ Example:
|
||||
},
|
||||
```
|
||||
|
||||
> ⚠️ DRM is not supported on visionOS yet
|
||||
|
||||
|
||||
#### Start playback at a specific point in time
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||
|
||||
Provide an optional `startPosition` for video. Value is in milliseconds. If the `cropStart` prop is applied, it will be applied from that point forward.
|
||||
(If it is negative or undefined or null, it is ignored)
|
||||
@ -828,6 +893,33 @@ source={{
|
||||
}}
|
||||
```
|
||||
|
||||
### `ad`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
|
||||
Sets the ad configuration.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
ad: {
|
||||
adTagUrl="https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpostoptimizedpodbumper&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator="
|
||||
adLanguage="fr"
|
||||
}
|
||||
```
|
||||
|
||||
See: [./ads.md] for more informations
|
||||
|
||||
Note: You need enable IMA SDK in gradle or pod file - [enable client side ads insertion](/installation)
|
||||
|
||||
|
||||
#### `contentStartTime`
|
||||
|
||||
<PlatformsList types={['Android']} />
|
||||
|
||||
The start time in ms for SSAI content. This determines at what time to load the video info like resolutions. Use this only when you have SSAI stream where ads resolution is not the same as content resolution.
|
||||
Note: This feature only works on DASH streams
|
||||
|
||||
#### `textTracksAllowChunklessPreparation`
|
||||
<PlatformsList types={['Android']} />
|
||||
|
||||
@ -843,37 +935,86 @@ source={{
|
||||
}}
|
||||
```
|
||||
|
||||
### `subtitleStyle`
|
||||
### `bufferConfig`
|
||||
|
||||
| Property | Description | Platforms |
|
||||
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
|
||||
| fontSize | Adjust the font size of the subtitles. Default: font size of the device | Android |
|
||||
| paddingTop | Adjust the top padding of the subtitles. Default: 0 | Android |
|
||||
| paddingBottom | Adjust the bottom padding of the subtitles. Default: 0 | Android |
|
||||
| paddingLeft | Adjust the left padding of the subtitles. Default: 0 | Android |
|
||||
| paddingRight | Adjust the right padding of the subtitles. Default: 0 | Android |
|
||||
| opacity | Adjust the visibility of subtitles with 0 hiding and 1 fully showing them. Android supports float values between 0 and 1 for varying opacity levels, whereas iOS supports only 0 or 1. Default: 1. | Android, iOS |
|
||||
<PlatformsList types={['Android']} />
|
||||
|
||||
Adjust the buffer settings. This prop takes an object with one or more of the properties listed below.
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| minBufferMs | number | The default minimum duration of media that the player will attempt to ensure is buffered at all times, in milliseconds. |
|
||||
| maxBufferMs | number | The default maximum duration of media that the player will attempt to buffer, in milliseconds. |
|
||||
| bufferForPlaybackMs | number | The default duration of media that must be buffered for playback to start or resume following a user action such as a seek, in milliseconds. |
|
||||
| bufferForPlaybackAfterRebufferMs | number | The default duration of media that must be buffered for playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action. |
|
||||
| backBufferDurationMs | number | The number of milliseconds of buffer to keep before the current position. This allows rewinding without rebuffering within that duration. |
|
||||
| maxHeapAllocationPercent | number | The percentage of available heap that the video can use to buffer, between 0 and 1 |
|
||||
| minBackBufferMemoryReservePercent | number | The percentage of available app memory at which during startup the back buffer will be disabled, between 0 and 1 |
|
||||
| minBufferMemoryReservePercent | number | The percentage of available app memory to keep in reserve that prevents buffer from using it, between 0 and 1 |
|
||||
| cacheSizeMB | number | Cache size in MB, enabling this to prevent new src requests and save bandwidth while repeating videos, or 0 to disable. Android only. |
|
||||
| live | object | Object containing another config set for live playback configuration, see next table |
|
||||
|
||||
|
||||
Description of live object:
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| maxPlaybackSpeed | number | The maximum playback speed the player can use to catch up when trying to reach the target live offset. |
|
||||
| minPlaybackSpeed | number | The minimum playback speed the player can use to fall back when trying to reach the target live offset. |
|
||||
| maxOffsetMs | number | The maximum allowed live offset. Even when adjusting the offset to current network conditions, the player will not attempt to get above this offset during playback. |
|
||||
| minOffsetMs | number | The minimum allowed live offset. Even when adjusting the offset to current network conditions, the player will not attempt to get below this offset during playback. |
|
||||
| targetOffsetMs | number | The target live offset. The player will attempt to get close to this live offset during playback if possible. |
|
||||
|
||||
For android, more informations about live configuration can be find [here](https://developer.android.com/media/media3/exoplayer/live-streaming?hl=en)
|
||||
|
||||
Example with default values:
|
||||
|
||||
```javascript
|
||||
bufferConfig={{
|
||||
minBufferMs: 15000,
|
||||
maxBufferMs: 50000,
|
||||
bufferForPlaybackMs: 2500,
|
||||
bufferForPlaybackAfterRebufferMs: 5000,
|
||||
backBufferDurationMs: 120000,
|
||||
cacheSizeMB: 0,
|
||||
live: {
|
||||
targetOffsetMs: 500,
|
||||
},
|
||||
}}
|
||||
```
|
||||
|
||||
Please note that the Android cache is a global cache that is shared among all components; individual components can still opt out of caching behavior by setting cacheSizeMB to 0, but multiple components with a positive cacheSizeMB will be sharing the same one, and the cache size will always be the first value set; it will not change during the app's lifecycle.
|
||||
|
||||
#### `minLoadRetryCount`
|
||||
|
||||
<PlatformsList types={['Android']} />
|
||||
|
||||
Sets the minimum number of times to retry loading data before failing and reporting an error to the application. Useful to recover from transient internet failures.
|
||||
|
||||
Default: 3. Retry 3 times.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
subtitleStyle={{ paddingBottom: 50, fontSize: 20, opacity: 0 }}
|
||||
source={{
|
||||
uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8',
|
||||
minLoadRetryCount={5} // retry 5 times
|
||||
}}
|
||||
```
|
||||
|
||||
### `textTracks`
|
||||
|
||||
#### `textTracks`
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||
|
||||
Load one or more "sidecar" text tracks. This takes an array of objects representing each track. Each object should have the format:
|
||||
|
||||
> ⚠️ This feature does not work with HLS playlists (e.g m3u8) on iOS
|
||||
|
||||
| Property | Description |
|
||||
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| title | Descriptive name for the track |
|
||||
| language | 2 letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) representing the language |
|
||||
| type | Mime type of the track _ TextTrackType.SRT - SubRip (.srt) _ TextTrackType.TTML - TTML (.ttml) \* TextTrackType.VTT - WebVTT (.vtt)iOS only supports VTT, Android supports all 3 |
|
||||
| uri | URL for the text track. Currently, only tracks hosted on a webserver are supported |
|
||||
| Property | Description |
|
||||
|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| title | Descriptive name for the track |
|
||||
| language | 2 letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) representing the language |
|
||||
| type | Mime type of the track _ TextTrackType.SUBRIP - SubRip (.srt) _ TextTrackType.TTML - TTML (.ttml) \* TextTrackType.VTT - WebVTT (.vtt)iOS only supports VTT, Android supports all 3 |
|
||||
| uri | URL for the text track. Currently, only tracks hosted on a webserver are supported |
|
||||
|
||||
On iOS, sidecar text tracks are only supported for individual files, not HLS playlists. For HLS, you should include the text tracks as part of the playlist.
|
||||
|
||||
@ -894,7 +1035,92 @@ textTracks={[
|
||||
{
|
||||
title: "Spanish Subtitles",
|
||||
language: "es",
|
||||
type: TextTrackType.SRT, // "application/x-subrip"
|
||||
type: TextTrackType.SUBRIP, // "application/x-subrip"
|
||||
uri: "https://durian.blender.org/wp-content/content/subtitles/sintel_es.srt"
|
||||
}
|
||||
]}
|
||||
```
|
||||
|
||||
### `subtitleStyle`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
|
||||
| Property | Platform | Description | Platforms |
|
||||
| ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
|
||||
| fontSize | Android | Adjust the font size of the subtitles. Default: font size of the device | Android |
|
||||
| paddingTop | Android | Adjust the top padding of the subtitles. Default: 0 | Android |
|
||||
| paddingBottom | Android | Adjust the bottom padding of the subtitles. Default: 0 | Android |
|
||||
| paddingLeft | Android | Adjust the left padding of the subtitles. Default: 0 | Android |
|
||||
| paddingRight | Android | Adjust the right padding of the subtitles. Default: 0 | Android |
|
||||
| opacity | Android, iOS | Adjust the visibility of subtitles with 0 hiding and 1 fully showing them. Android supports float values between 0 and 1 for varying opacity levels, whereas iOS supports only 0 or 1. Default: 1. | Android, iOS |
|
||||
| subtitlesFollowVideo | Android | Boolean to adjust position of subtitles. Default: true |
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
subtitleStyle={{ paddingBottom: 50, fontSize: 20, opacity: 0 }}
|
||||
```
|
||||
|
||||
Note for `subtitlesFollowVideo`
|
||||
|
||||
`subtitlesFollowVideo` helps to determine how the subtitles are positionned.
|
||||
To understand this prop you need to understand how views management works.
|
||||
The main View style passed to react native video is the position reserved to display the video component.
|
||||
It may not match exactly the real video size.
|
||||
For exemple, you can pass a 4:3 video view and render a 16:9 video inside.
|
||||
So there is a second view, the video view.
|
||||
|
||||
Subtitles are managed in a third view.
|
||||
|
||||
First react-native-video resize the video to keep aspect ratio (depending on `resizeMode` property) and put it in main view.
|
||||
|
||||
* When putting subtitlesFollowVideo to true, the subtitle view will be adapt to the video view.
|
||||
It means that if the video is displayed out of screen, the subtitles may also be displayed out of screen.
|
||||
|
||||
* When putting subtitlesFollowVideo to false, the subtitle view will keep adapting to the main view.
|
||||
It means that if the video is displayed out of screen, the subtitles may also be displayed out of screen.
|
||||
|
||||
This prop can be changed on runtime.
|
||||
|
||||
### `textTracks`
|
||||
|
||||
> [!WARNING]
|
||||
> deprecated, use source.textTracks instead. changing text tracks will restart playback
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||
|
||||
Load one or more "sidecar" text tracks. This takes an array of objects representing each track. Each object should have the format:
|
||||
|
||||
> ⚠️ This feature does not work with HLS playlists (e.g m3u8) on iOS
|
||||
|
||||
| Property | Description |
|
||||
|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| title | Descriptive name for the track |
|
||||
| language | 2 letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) representing the language |
|
||||
| type | Mime type of the track _ TextTrackType.SUBRIP - SubRip (.srt) _ TextTrackType.TTML - TTML (.ttml) \* TextTrackType.VTT - WebVTT (.vtt)iOS only supports VTT, Android supports all 3 |
|
||||
| uri | URL for the text track. Currently, only tracks hosted on a webserver are supported |
|
||||
|
||||
On iOS, sidecar text tracks are only supported for individual files, not HLS playlists. For HLS, you should include the text tracks as part of the playlist.
|
||||
|
||||
Note: Due to iOS limitations, sidecar text tracks are not compatible with Airplay. If textTracks are specified, AirPlay support will be automatically disabled.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
import { TextTrackType }, Video from 'react-native-video';
|
||||
|
||||
textTracks={[
|
||||
{
|
||||
title: "English CC",
|
||||
language: "en",
|
||||
type: TextTrackType.VTT, // "text/vtt"
|
||||
uri: "https://bitdash-a.akamaihd.net/content/sintel/subtitles/subtitles_en.vtt"
|
||||
},
|
||||
{
|
||||
title: "Spanish Subtitles",
|
||||
language: "es",
|
||||
type: TextTrackType.SUBRIP, // "application/x-subrip"
|
||||
uri: "https://durian.blender.org/wp-content/content/subtitles/sintel_es.srt"
|
||||
}
|
||||
]}
|
||||
@ -902,7 +1128,7 @@ textTracks={[
|
||||
|
||||
### `showNotificationControls`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||
|
||||
Controls whether to show media controls in the notification area.
|
||||
For Android each Video component will have its own notification controls and for iOS only one notification control will be shown for the last Active Video component.
|
||||
@ -999,3 +1225,68 @@ Adjust the volume.
|
||||
- **1.0 (default)** - Play at full volume
|
||||
- **0.0** - Mute the audio
|
||||
- **Other values** - Reduce volume
|
||||
|
||||
### `cmcd`
|
||||
|
||||
<PlatformsList types={['Android']} />
|
||||
|
||||
Configure CMCD (Common Media Client Data) parameters. CMCD is a standard for conveying client-side metrics and capabilities to servers, which can help improve streaming quality and performance.
|
||||
|
||||
For detailed information about CMCD, please refer to the [CTA-5004 Final Specification](https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf).
|
||||
|
||||
- **false (default)** - Don't use CMCD
|
||||
- **true** - Use default CMCD configuration
|
||||
- **object** - Use custom CMCD configuration
|
||||
|
||||
When providing an object, you can configure the following properties:
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|-------------------------|----------------------------------------------------|
|
||||
| `mode` | `CmcdMode` | The mode for sending CMCD data |
|
||||
| `request` | `CmcdData` | Custom key-value pairs for the request object |
|
||||
| `session` | `CmcdData` | Custom key-value pairs for the session object |
|
||||
| `object` | `CmcdData` | Custom key-value pairs for the object metadata |
|
||||
| `status` | `CmcdData` | Custom key-value pairs for the status information |
|
||||
|
||||
Note: The `mode` property defaults to `CmcdMode.MODE_QUERY_PARAMETER` if not specified.
|
||||
|
||||
#### `CmcdMode`
|
||||
CmcdMode is an enum that defines how CMCD data should be sent:
|
||||
- `CmcdMode.MODE_REQUEST_HEADER` (0) - Send CMCD data in the HTTP request headers.
|
||||
- `CmcdMode.MODE_QUERY_PARAMETER` (1) - Send CMCD data as query parameters in the URL.
|
||||
|
||||
#### `CmcdData`
|
||||
CmcdData is a type representing custom key-value pairs for CMCD data. It's defined as:
|
||||
|
||||
```typescript
|
||||
type CmcdData = Record<`${string}-${string}`, string | number>;
|
||||
```
|
||||
|
||||
Custom key names MUST include a hyphenated prefix to prevent namespace collisions. It's recommended to use a reverse-DNS syntax for custom prefixes.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
<Video
|
||||
source={{
|
||||
uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8',
|
||||
cmcd: {
|
||||
mode: CmcdMode.MODE_QUERY_PARAMETER,
|
||||
request: {
|
||||
'com-custom-key': 'custom-value'
|
||||
},
|
||||
session: {
|
||||
sid: 'session-id'
|
||||
},
|
||||
object: {
|
||||
br: '3000',
|
||||
d: '4000'
|
||||
},
|
||||
status: {
|
||||
rtp: '1200'
|
||||
}
|
||||
}
|
||||
}}
|
||||
// or other video props
|
||||
/>
|
||||
```
|
||||
|
@ -8,11 +8,12 @@ It allows to stream video files (m3u, mpd, mp4, ...) inside your react native ap
|
||||
- Exoplayer for android
|
||||
- AVplayer for iOS, tvOS and visionOS
|
||||
- Windows UWP for windows
|
||||
- HTML5 for web
|
||||
- Trick mode support
|
||||
- Subtitles (embeded or side loaded)
|
||||
- DRM support
|
||||
- Client side Ads insertion (via google IMA)
|
||||
- Pip (ios)
|
||||
- Pip
|
||||
- Embedded playback controls
|
||||
- And much more
|
||||
|
||||
|
@ -181,3 +181,12 @@ Select RCTVideo-tvOS
|
||||
Run `pod install` in the `visionos` directory of your project
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>web</summary>
|
||||
|
||||
Nothing to do, everything should work out of the box.
|
||||
|
||||
Note that only basic video support is present, no hls/dash or ads/drm for now.
|
||||
|
||||
</details>
|
||||
|
@ -86,11 +86,6 @@ buildscript {
|
||||
}
|
||||
```
|
||||
|
||||
### Desugaring
|
||||
to be able to link you may also need to enable coreLibraryDesugaringEnabled in your app.
|
||||
|
||||
See: https://developer.android.com/studio/write/java8-support?hl=fr#library-desugaring for more informations.
|
||||
|
||||
## It's still not working
|
||||
|
||||
You can try to open a ticket now !
|
||||
You can try to open a ticket or contact us for [premium support](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs#Contact)!
|
||||
|
@ -37,4 +37,5 @@ It's useful when you are using `expo` managed workflow (expo prebuild) as it wil
|
||||
| enableBackgroundAudio | boolean | false | Add required changes to play video in background on iOS |
|
||||
| enableADSExtension | boolean | false | Add required changes to use ads extension for video player |
|
||||
| enableCacheExtension | boolean | false | Add required changes to use cache extension for video player on iOS |
|
||||
| androidExtensions | object | {} | You can enable/disable extensions as per your requirement - this allow to reduce library size on android |
|
||||
| androidExtensions | object | {} | You can enable/disable extensions as per your requirement - this allow to reduce library size on android |
|
||||
| enableAndroidPictureInPicture | boolean | false | Apply configs to be able to use Picture-in-picture on android |
|
@ -2,10 +2,11 @@
|
||||
This page links other open source projects which can be useful for your player implementation. <br>
|
||||
If you have a project which can be useful for other users, feel free to open a PR to add it here.
|
||||
|
||||
## UI over react-native-video
|
||||
- [react-native-video-controls](https://github.com/itsnubix/react-native-video-controls): First reference player UI
|
||||
- [react-native-media-console](https://github.com/criszz77/react-native-media-console): React-native-video-controls updated and rewritten in typescript
|
||||
- [react-native-corner-video](https://github.com/Lg0gs/react-native-corner-video): A floating video player
|
||||
## Our (TheWidlarzGroup) libraries
|
||||
- [react-native-video-player](https://github.com/TheWidlarzGroup/react-native-video-player): Our video player UI library
|
||||
|
||||
## Other tools
|
||||
- [react-native-track-player](https://github.com/doublesymmetry/react-native-track-player): A toolbox to control player over media session
|
||||
## Community libraries
|
||||
- [react-native-corner-video](https://github.com/Lg0gs/react-native-corner-video): A floating video player
|
||||
- [react-native-track-player](https://github.com/doublesymmetry/react-native-track-player): A toolbox for audio playback
|
||||
- [react-native-video-controls](https://github.com/itsnubix/react-native-video-controls): Video player UI
|
||||
- [react-native-media-console](https://github.com/criszz77/react-native-media-console): React-native-video-controls updated and rewritten in typescript
|
||||
|
@ -22,7 +22,7 @@ In your project Podfile add support for static dependency linking. This is requi
|
||||
|
||||
Add `use_frameworks! :linkage => :static` just under `platform :ios` in your ios project Podfile.
|
||||
|
||||
[See the example ios project for reference](examples/basic/ios/Podfile#L5)
|
||||
[See the example ios project for reference](https://github.com/TheWidlarzGroup/react-native-video/blob/master/examples/basic/ios/Podfile#L5)
|
||||
|
||||
##### podspec
|
||||
|
||||
@ -34,7 +34,7 @@ You can remove following lines from your podfile as they are not necessary anymo
|
||||
- `pod 'react-native-video/VideoCaching', :path => '../node_modules/react-native-video/react-native-video.podspec'`
|
||||
```
|
||||
|
||||
If you were previously using VideoCaching, you should $RNVideoUseVideoCaching flag in your podspec, see: [installation section](https://react-native-video.github.io/react-native-video/installation#video-caching)
|
||||
If you were previously using VideoCaching, you should $RNVideoUseVideoCaching flag in your podspec, see: [installation section](https://docs.thewidlarzgroup.com/react-native-video/installation#video-caching)
|
||||
|
||||
#### Android
|
||||
|
||||
@ -66,4 +66,4 @@ allprojects {
|
||||
}
|
||||
}
|
||||
```
|
||||
If you encounter an error `Could not find com.android.support:support-annotations:27.0.0.` reinstall your Android Support Repository.
|
||||
If you encounter an error `Could not find com.android.support:support-annotations:27.0.0.` reinstall your Android Support Repository.
|
||||
|
BIN
docs/public/favicon.png
Normal file
BIN
docs/public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import TWGBadge from './components/TWGBadge/TWGBadge';
|
||||
|
||||
export default {
|
||||
head: (
|
||||
@ -13,7 +14,7 @@ export default {
|
||||
/>
|
||||
<meta
|
||||
name="og:image"
|
||||
content="https://thewidlarzgroup.github.io/react-native-video/thumbnail.jpg"
|
||||
content="https://docs.thewidlarzgroup.com/react-native-video/thumbnail.jpg"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="React Native Video" />
|
||||
@ -23,15 +24,26 @@ export default {
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://thewidlarzgroup.github.io/react-native-video/thumbnail.jpg"
|
||||
content="https://docs.thewidlarzgroup.com/react-native-video/thumbnail.jpg"
|
||||
/>
|
||||
<meta name="twitter:image:alt" content="React Native Video" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400..900&display=swap"
|
||||
rel="stylesheet"
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
href="https://docs.thewidlarzgroup.com/react-native-video/favicon.png"
|
||||
/>
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-4YEWQH5ZHS"
|
||||
/>
|
||||
<script>
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-4YEWQH5ZHS');
|
||||
`}
|
||||
</script>
|
||||
</>
|
||||
),
|
||||
logo: (
|
||||
@ -39,12 +51,20 @@ export default {
|
||||
🎬 <strong>Video component</strong> for React Native
|
||||
</span>
|
||||
),
|
||||
faviconGlyph: '🎬',
|
||||
project: {
|
||||
link: 'https://github.com/TheWidlarzGroup/react-native-video',
|
||||
},
|
||||
docsRepositoryBase:
|
||||
'https://github.com/TheWidlarzGroup/react-native-video/tree/master/docs/',
|
||||
main: ({children}) => (
|
||||
<>
|
||||
{children}
|
||||
<TWGBadge visibleOnLarge={false} />
|
||||
</>
|
||||
),
|
||||
toc: {
|
||||
extraContent: <TWGBadge visibleOnLarge={true} />,
|
||||
},
|
||||
footer: {
|
||||
text: (
|
||||
<span>
|
||||
@ -52,6 +72,7 @@ export default {
|
||||
</span>
|
||||
),
|
||||
},
|
||||
|
||||
useNextSeoProps() {
|
||||
return {
|
||||
titleTemplate: '%s – Video',
|
||||
|
@ -1,6 +0,0 @@
|
||||
|
||||
[android]
|
||||
target = Google Inc.:Google APIs:23
|
||||
|
||||
[maven_repositories]
|
||||
central = https://repo1.maven.org/maven2
|
@ -1,2 +0,0 @@
|
||||
BUNDLE_PATH: "vendor/bundle"
|
||||
BUNDLE_FORCE_RUBY_PLATFORM: 1
|
@ -1,16 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: '@react-native',
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-shadow': ['error'],
|
||||
'no-shadow': 'off',
|
||||
'no-undef': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
67
examples/FabricExample/.gitignore
vendored
67
examples/FabricExample/.gitignore
vendored
@ -1,67 +0,0 @@
|
||||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Xcode
|
||||
#
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
ios/.xcode.env.local
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
|
||||
# node.js
|
||||
#
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# BUCK
|
||||
buck-out/
|
||||
\.buckd/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/
|
||||
|
||||
**/fastlane/report.xml
|
||||
**/fastlane/Preview.html
|
||||
**/fastlane/screenshots
|
||||
**/fastlane/test_output
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
# Ruby / CocoaPods
|
||||
/ios/Pods/
|
||||
/vendor/bundle/
|
||||
|
||||
# testing
|
||||
/coverage
|
@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
arrowParens: 'avoid',
|
||||
bracketSameLine: true,
|
||||
bracketSpacing: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
};
|
@ -1 +0,0 @@
|
||||
2.7.5
|
@ -1 +0,0 @@
|
||||
{}
|
@ -1,19 +0,0 @@
|
||||
/**
|
||||
* Sample React Native App
|
||||
* https://github.com/facebook/react-native
|
||||
*
|
||||
* Generated with the TypeScript template
|
||||
* https://github.com/react-native-community/react-native-template-typescript
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import React, {type PropsWithChildren} from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import VideoPlayer from './src/VideoPlayer';
|
||||
|
||||
const App = () => {
|
||||
return <VideoPlayer />;
|
||||
};
|
||||
|
||||
export default App;
|
@ -1,7 +0,0 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
||||
ruby ">= 2.6.10"
|
||||
|
||||
gem 'cocoapods', '~> 1.13'
|
||||
gem 'activesupport', '>= 6.1.7.3', '< 7.1.0'
|
@ -1,107 +0,0 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.7)
|
||||
base64
|
||||
nkf
|
||||
rexml
|
||||
activesupport (6.1.7.8)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
zeitwerk (~> 2.3)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
algoliasearch (1.27.5)
|
||||
httpclient (~> 2.8, >= 2.8.3)
|
||||
json (>= 1.5.1)
|
||||
atomos (0.1.3)
|
||||
base64 (0.2.0)
|
||||
claide (1.1.0)
|
||||
cocoapods (1.15.2)
|
||||
addressable (~> 2.8)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.15.2)
|
||||
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
||||
cocoapods-downloader (>= 2.1, < 3.0)
|
||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||
cocoapods-search (>= 1.0.0, < 2.0)
|
||||
cocoapods-trunk (>= 1.6.0, < 2.0)
|
||||
cocoapods-try (>= 1.1.0, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
escape (~> 0.0.4)
|
||||
fourflusher (>= 2.3.0, < 3.0)
|
||||
gh_inspector (~> 1.0)
|
||||
molinillo (~> 0.8.0)
|
||||
nap (~> 1.0)
|
||||
ruby-macho (>= 2.3.0, < 3.0)
|
||||
xcodeproj (>= 1.23.0, < 2.0)
|
||||
cocoapods-core (1.15.2)
|
||||
activesupport (>= 5.0, < 8)
|
||||
addressable (~> 2.8)
|
||||
algoliasearch (~> 1.0)
|
||||
concurrent-ruby (~> 1.1)
|
||||
fuzzy_match (~> 2.0.4)
|
||||
nap (~> 1.0)
|
||||
netrc (~> 0.11)
|
||||
public_suffix (~> 4.0)
|
||||
typhoeus (~> 1.0)
|
||||
cocoapods-deintegrate (1.0.5)
|
||||
cocoapods-downloader (2.1)
|
||||
cocoapods-plugins (1.0.0)
|
||||
nap
|
||||
cocoapods-search (1.0.1)
|
||||
cocoapods-trunk (1.6.0)
|
||||
nap (>= 0.8, < 2.0)
|
||||
netrc (~> 0.11)
|
||||
cocoapods-try (1.2.0)
|
||||
colored2 (3.1.2)
|
||||
concurrent-ruby (1.2.2)
|
||||
escape (0.0.4)
|
||||
ethon (0.16.0)
|
||||
ffi (>= 1.15.0)
|
||||
ffi (1.17.0)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.12.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.7.2)
|
||||
minitest (5.18.0)
|
||||
molinillo (0.8.0)
|
||||
nanaimo (0.3.0)
|
||||
nap (1.1.0)
|
||||
netrc (0.11.0)
|
||||
nkf (0.2.0)
|
||||
public_suffix (4.0.7)
|
||||
rexml (3.2.9)
|
||||
strscan
|
||||
ruby-macho (2.5.1)
|
||||
strscan (3.1.0)
|
||||
typhoeus (1.4.1)
|
||||
ethon (>= 0.9.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
xcodeproj (1.24.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
zeitwerk (2.6.16)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
activesupport (>= 6.1.7.3, < 7.1.0)
|
||||
cocoapods (~> 1.13)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.7.5p203
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.5
|
@ -1,17 +0,0 @@
|
||||
/**
|
||||
* @format
|
||||
*/
|
||||
|
||||
import 'react-native';
|
||||
import React from 'react';
|
||||
import App from '../App';
|
||||
|
||||
// Note: import explicitly to use the types shipped with jest.
|
||||
import {it} from '@jest/globals';
|
||||
|
||||
// Note: test renderer must be required after react-native.
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
it('renders correctly', () => {
|
||||
renderer.create(<App />);
|
||||
});
|
@ -1,55 +0,0 @@
|
||||
# To learn about Buck see [Docs](https://buckbuild.com/).
|
||||
# To run your application with Buck:
|
||||
# - install Buck
|
||||
# - `npm start` - to start the packager
|
||||
# - `cd android`
|
||||
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
|
||||
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
|
||||
# - `buck install -r android/app` - compile, install and run application
|
||||
#
|
||||
|
||||
load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
|
||||
|
||||
lib_deps = []
|
||||
|
||||
create_aar_targets(glob(["libs/*.aar"]))
|
||||
|
||||
create_jar_targets(glob(["libs/*.jar"]))
|
||||
|
||||
android_library(
|
||||
name = "all-libs",
|
||||
exported_deps = lib_deps,
|
||||
)
|
||||
|
||||
android_library(
|
||||
name = "app-code",
|
||||
srcs = glob([
|
||||
"src/main/java/**/*.java",
|
||||
]),
|
||||
deps = [
|
||||
":all-libs",
|
||||
":build_config",
|
||||
":res",
|
||||
],
|
||||
)
|
||||
|
||||
android_build_config(
|
||||
name = "build_config",
|
||||
package = "net.video.fabricexample",
|
||||
)
|
||||
|
||||
android_resource(
|
||||
name = "res",
|
||||
package = "net.video.fabricexample",
|
||||
res = "src/main/res",
|
||||
)
|
||||
|
||||
android_binary(
|
||||
name = "app",
|
||||
keystore = "//android/keystores:debug",
|
||||
manifest = "src/main/AndroidManifest.xml",
|
||||
package_type = "debug",
|
||||
deps = [
|
||||
":app-code",
|
||||
],
|
||||
)
|
@ -1,123 +0,0 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
|
||||
import com.android.build.OutputFile
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||
*/
|
||||
react {
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '..'
|
||||
// root = file("../")
|
||||
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
|
||||
// reactNativeDir = file("../node-modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
|
||||
// codegenDir = file("../node_modules/@react-native/codegen")
|
||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
|
||||
// cliFile = file("../node_modules/react-native/cli.js")
|
||||
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
||||
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
// nodeExecutableAndArgs = ["node"]
|
||||
//
|
||||
// The command to run when bundling. By default is 'bundle'
|
||||
// bundleCommand = "ram-bundle"
|
||||
//
|
||||
// The path to the CLI configuration file. Default is empty.
|
||||
// bundleConfig = file(../rn-cli.config.js)
|
||||
//
|
||||
// The name of the generated asset file containing your JS bundle
|
||||
// bundleAssetName = "MyApplication.android.bundle"
|
||||
//
|
||||
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
||||
// entryFile = file("../js/MyApplication.android.js")
|
||||
//
|
||||
// A list of extra flags to pass to the 'bundle' commands.
|
||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// extraPackagerArgs = []
|
||||
|
||||
/* Hermes Commands */
|
||||
// The hermes compiler command to run. By default it is 'hermesc'
|
||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = false
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
|
||||
namespace "net.video.fabricexample"
|
||||
defaultConfig {
|
||||
applicationId "net.video.fabricexample"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
implementation("com.facebook.react:flipper-integration")
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
|
||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
@ -1,19 +0,0 @@
|
||||
"""Helper definitions to glob .aar and .jar targets"""
|
||||
|
||||
def create_aar_targets(aarfiles):
|
||||
for aarfile in aarfiles:
|
||||
name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")]
|
||||
lib_deps.append(":" + name)
|
||||
android_prebuilt_aar(
|
||||
name = name,
|
||||
aar = aarfile,
|
||||
)
|
||||
|
||||
def create_jar_targets(jarfiles):
|
||||
for jarfile in jarfiles:
|
||||
name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")]
|
||||
lib_deps.append(":" + name)
|
||||
prebuilt_jar(
|
||||
name = name,
|
||||
binary_jar = jarfile,
|
||||
)
|
@ -1,10 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
@ -1,26 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="net.video.fabricexample">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
@ -1,22 +0,0 @@
|
||||
package net.video.fabricexample
|
||||
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "FabricExample"
|
||||
|
||||
/**
|
||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate =
|
||||
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package net.video.fabricexample
|
||||
|
||||
import android.app.Application
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.ReactNativeHost
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.react.flipper.ReactNativeFlipper
|
||||
import com.facebook.soloader.SoLoader
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
|
||||
override val reactNativeHost: ReactNativeHost =
|
||||
object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> =
|
||||
PackageList(this).packages.apply {
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// add(MyReactNativePackage())
|
||||
}
|
||||
|
||||
override fun getJSMainModuleName(): String = "index"
|
||||
|
||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
||||
|
||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
||||
}
|
||||
|
||||
override val reactHost: ReactHost
|
||||
get() = getDefaultReactHost(this.applicationContext, reactNativeHost)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
SoLoader.init(this, false)
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load()
|
||||
}
|
||||
ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager)
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
# Define the library name here.
|
||||
project(fabricexample_appmodules)
|
||||
|
||||
# This file includes all the necessary to let you build your application with the New Architecture.
|
||||
include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)
|
@ -1,32 +0,0 @@
|
||||
#include "MainApplicationModuleProvider.h"
|
||||
|
||||
#include <rncli.h>
|
||||
#include <rncore.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
|
||||
const std::string &moduleName,
|
||||
const JavaTurboModule::InitParams ¶ms) {
|
||||
// Here you can provide your own module provider for TurboModules coming from
|
||||
// either your application or from external libraries. The approach to follow
|
||||
// is similar to the following (for a library called `samplelibrary`:
|
||||
//
|
||||
// auto module = samplelibrary_ModuleProvider(moduleName, params);
|
||||
// if (module != nullptr) {
|
||||
// return module;
|
||||
// }
|
||||
// return rncore_ModuleProvider(moduleName, params);
|
||||
|
||||
// Module providers autolinked by RN CLI
|
||||
auto rncli_module = rncli_ModuleProvider(moduleName, params);
|
||||
if (rncli_module != nullptr) {
|
||||
return rncli_module;
|
||||
}
|
||||
|
||||
return rncore_ModuleProvider(moduleName, params);
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <ReactCommon/JavaTurboModule.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
|
||||
const std::string &moduleName,
|
||||
const JavaTurboModule::InitParams ¶ms);
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
@ -1,45 +0,0 @@
|
||||
#include "MainApplicationTurboModuleManagerDelegate.h"
|
||||
#include "MainApplicationModuleProvider.h"
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
jni::local_ref<MainApplicationTurboModuleManagerDelegate::jhybriddata>
|
||||
MainApplicationTurboModuleManagerDelegate::initHybrid(
|
||||
jni::alias_ref<jhybridobject>) {
|
||||
return makeCxxInstance();
|
||||
}
|
||||
|
||||
void MainApplicationTurboModuleManagerDelegate::registerNatives() {
|
||||
registerHybrid({
|
||||
makeNativeMethod(
|
||||
"initHybrid", MainApplicationTurboModuleManagerDelegate::initHybrid),
|
||||
makeNativeMethod(
|
||||
"canCreateTurboModule",
|
||||
MainApplicationTurboModuleManagerDelegate::canCreateTurboModule),
|
||||
});
|
||||
}
|
||||
|
||||
std::shared_ptr<TurboModule>
|
||||
MainApplicationTurboModuleManagerDelegate::getTurboModule(
|
||||
const std::string &name,
|
||||
const std::shared_ptr<CallInvoker> &jsInvoker) {
|
||||
// Not implemented yet: provide pure-C++ NativeModules here.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<TurboModule>
|
||||
MainApplicationTurboModuleManagerDelegate::getTurboModule(
|
||||
const std::string &name,
|
||||
const JavaTurboModule::InitParams ¶ms) {
|
||||
return MainApplicationModuleProvider(name, params);
|
||||
}
|
||||
|
||||
bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule(
|
||||
const std::string &name) {
|
||||
return getTurboModule(name, nullptr) != nullptr ||
|
||||
getTurboModule(name, {.moduleName = name}) != nullptr;
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
@ -1,38 +0,0 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <ReactCommon/TurboModuleManagerDelegate.h>
|
||||
#include <fbjni/fbjni.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
class MainApplicationTurboModuleManagerDelegate
|
||||
: public jni::HybridClass<
|
||||
MainApplicationTurboModuleManagerDelegate,
|
||||
TurboModuleManagerDelegate> {
|
||||
public:
|
||||
// Adapt it to the package you used for your Java class.
|
||||
static constexpr auto kJavaDescriptor =
|
||||
"Lnet/video/fabricexample/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;";
|
||||
|
||||
static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject>);
|
||||
|
||||
static void registerNatives();
|
||||
|
||||
std::shared_ptr<TurboModule> getTurboModule(
|
||||
const std::string &name,
|
||||
const std::shared_ptr<CallInvoker> &jsInvoker) override;
|
||||
std::shared_ptr<TurboModule> getTurboModule(
|
||||
const std::string &name,
|
||||
const JavaTurboModule::InitParams ¶ms) override;
|
||||
|
||||
/**
|
||||
* Test-only method. Allows user to verify whether a TurboModule can be
|
||||
* created by instances of this class.
|
||||
*/
|
||||
bool canCreateTurboModule(const std::string &name);
|
||||
};
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
@ -1,65 +0,0 @@
|
||||
#include "MainComponentsRegistry.h"
|
||||
|
||||
#include <CoreComponentsRegistry.h>
|
||||
#include <fbjni/fbjni.h>
|
||||
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
|
||||
#include <react/renderer/components/rncore/ComponentDescriptors.h>
|
||||
#include <rncli.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
MainComponentsRegistry::MainComponentsRegistry(ComponentFactory *delegate) {}
|
||||
|
||||
std::shared_ptr<ComponentDescriptorProviderRegistry const>
|
||||
MainComponentsRegistry::sharedProviderRegistry() {
|
||||
auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry();
|
||||
|
||||
// Autolinked providers registered by RN CLI
|
||||
rncli_registerProviders(providerRegistry);
|
||||
|
||||
// Custom Fabric Components go here. You can register custom
|
||||
// components coming from your App or from 3rd party libraries here.
|
||||
//
|
||||
// providerRegistry->add(concreteComponentDescriptorProvider<
|
||||
// AocViewerComponentDescriptor>());
|
||||
return providerRegistry;
|
||||
}
|
||||
|
||||
jni::local_ref<MainComponentsRegistry::jhybriddata>
|
||||
MainComponentsRegistry::initHybrid(
|
||||
jni::alias_ref<jclass>,
|
||||
ComponentFactory *delegate) {
|
||||
auto instance = makeCxxInstance(delegate);
|
||||
|
||||
auto buildRegistryFunction =
|
||||
[](EventDispatcher::Weak const &eventDispatcher,
|
||||
ContextContainer::Shared const &contextContainer)
|
||||
-> ComponentDescriptorRegistry::Shared {
|
||||
auto registry = MainComponentsRegistry::sharedProviderRegistry()
|
||||
->createComponentDescriptorRegistry(
|
||||
{eventDispatcher, contextContainer});
|
||||
|
||||
auto mutableRegistry =
|
||||
std::const_pointer_cast<ComponentDescriptorRegistry>(registry);
|
||||
|
||||
mutableRegistry->setFallbackComponentDescriptor(
|
||||
std::make_shared<UnimplementedNativeViewComponentDescriptor>(
|
||||
ComponentDescriptorParameters{
|
||||
eventDispatcher, contextContainer, nullptr}));
|
||||
|
||||
return registry;
|
||||
};
|
||||
|
||||
delegate->buildRegistryFunction = buildRegistryFunction;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void MainComponentsRegistry::registerNatives() {
|
||||
registerHybrid({
|
||||
makeNativeMethod("initHybrid", MainComponentsRegistry::initHybrid),
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
@ -1,32 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <ComponentFactory.h>
|
||||
#include <fbjni/fbjni.h>
|
||||
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
|
||||
#include <react/renderer/componentregistry/ComponentDescriptorRegistry.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
class MainComponentsRegistry
|
||||
: public facebook::jni::HybridClass<MainComponentsRegistry> {
|
||||
public:
|
||||
// Adapt it to the package you used for your Java class.
|
||||
constexpr static auto kJavaDescriptor =
|
||||
"Lnet/video/fabricexample/newarchitecture/components/MainComponentsRegistry;";
|
||||
|
||||
static void registerNatives();
|
||||
|
||||
MainComponentsRegistry(ComponentFactory *delegate);
|
||||
|
||||
private:
|
||||
static std::shared_ptr<ComponentDescriptorProviderRegistry const>
|
||||
sharedProviderRegistry();
|
||||
|
||||
static jni::local_ref<jhybriddata> initHybrid(
|
||||
jni::alias_ref<jclass>,
|
||||
ComponentFactory *delegate);
|
||||
};
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
@ -1,11 +0,0 @@
|
||||
#include <fbjni/fbjni.h>
|
||||
#include "MainApplicationTurboModuleManagerDelegate.h"
|
||||
#include "MainComponentsRegistry.h"
|
||||
|
||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
|
||||
return facebook::jni::initialize(vm, [] {
|
||||
facebook::react::MainApplicationTurboModuleManagerDelegate::
|
||||
registerNatives();
|
||||
facebook::react::MainComponentsRegistry::registerNatives();
|
||||
});
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetTop="@dimen/abc_edit_text_inset_top_material"
|
||||
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material">
|
||||
|
||||
<selector>
|
||||
<!--
|
||||
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
|
||||
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
|
||||
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
|
||||
|
||||
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
|
||||
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
|
||||
-->
|
||||
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
|
||||
</selector>
|
||||
|
||||
</inset>
|
@ -1,3 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">FabricExample</string>
|
||||
</resources>
|
@ -1,9 +0,0 @@
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user