) {}
19 | //
20 | get dataSource(): { [key in string]: number } {
21 | return {};
22 | }
23 |
24 | set dataSource(_data: { [key in string]: number }) {}
25 | }
26 |
--------------------------------------------------------------------------------
/src/user-defined-guard/index.ts:
--------------------------------------------------------------------------------
1 | import type { MemoryCachePolicyInterface } from '../types/type';
2 |
3 | export function isMemoryCachePolicyInterface(
4 | policy: any
5 | ): policy is MemoryCachePolicyInterface {
6 | const hasDataSourceGet =
7 | Object.getOwnPropertyDescriptor(policy, 'dataSource')?.get ||
8 | Object.getOwnPropertyDescriptor(Object.getPrototypeOf(policy), 'dataSource')
9 | ?.get;
10 |
11 | const hasDataSourceSet =
12 | Object.getOwnPropertyDescriptor(policy, 'dataSource')?.set ||
13 | Object.getOwnPropertyDescriptor(Object.getPrototypeOf(policy), 'dataSource')
14 | ?.set;
15 |
16 | return (
17 | policy &&
18 | typeof policy.onAccess === 'function' &&
19 | typeof policy.onEvict === 'function' &&
20 | hasDataSourceGet &&
21 | hasDataSourceSet
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/example/android/app/src/release/java/com/cachevideoexample/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Meta Platforms, Inc. and affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the LICENSE file in the root
5 | * directory of this source tree.
6 | */
7 | package com.cachevideoexample;
8 |
9 | import android.content.Context;
10 | import com.facebook.react.ReactInstanceManager;
11 |
12 | /**
13 | * Class responsible of loading Flipper inside your React Native application. This is the release
14 | * flavor of it so it's empty as we don't want to load Flipper.
15 | */
16 | public class ReactNativeFlipper {
17 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
18 | // Do nothing as we don't want to initialize Flipper on Release.
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "pipeline": {
4 | "build:android": {
5 | "inputs": [
6 | "package.json",
7 | "android",
8 | "!android/build",
9 | "src/*.ts",
10 | "src/*.tsx",
11 | "example/package.json",
12 | "example/android",
13 | "!example/android/.gradle",
14 | "!example/android/build",
15 | "!example/android/app/build"
16 | ],
17 | "outputs": []
18 | },
19 | "build:ios": {
20 | "inputs": [
21 | "package.json",
22 | "*.podspec",
23 | "ios",
24 | "src/*.ts",
25 | "src/*.tsx",
26 | "example/package.json",
27 | "example/ios",
28 | "!example/ios/build",
29 | "!example/ios/Pods"
30 | ],
31 | "outputs": []
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/example/ios/CacheVideoExampleTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.github/actions/setup/action.yml:
--------------------------------------------------------------------------------
1 | name: Setup
2 | description: Setup Node.js and install dependencies
3 |
4 | runs:
5 | using: composite
6 | steps:
7 | - name: Setup Node.js
8 | uses: actions/setup-node@v3
9 | with:
10 | node-version-file: .nvmrc
11 |
12 | - name: Cache dependencies
13 | id: yarn-cache
14 | uses: actions/cache@v3
15 | with:
16 | path: |
17 | **/node_modules
18 | .yarn/install-state.gz
19 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/package.json') }}
20 | restore-keys: |
21 | ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
22 | ${{ runner.os }}-yarn-
23 |
24 | - name: Install dependencies
25 | if: steps.yarn-cache.outputs.cache-hit != 'true'
26 | run: yarn install --immutable
27 | shell: bash
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "rootDir": ".",
4 | "paths": {
5 | "react-native-cache-video": ["./src/index"]
6 | },
7 | "allowJs": true,
8 | "allowUnreachableCode": false,
9 | "allowUnusedLabels": false,
10 | "esModuleInterop": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "jsx": "react",
13 | "lib": ["esnext"],
14 | "module": "esnext",
15 | "moduleResolution": "node",
16 | "noFallthroughCasesInSwitch": true,
17 | "noImplicitReturns": true,
18 | "noImplicitUseStrict": false,
19 | "noStrictGenericChecks": false,
20 | "noUncheckedIndexedAccess": true,
21 | "noUnusedLocals": true,
22 | "noUnusedParameters": true,
23 | "resolveJsonModule": true,
24 | "skipLibCheck": true,
25 | "strict": true,
26 | "target": "esnext",
27 | "verbatimModuleSyntax": true
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/example/ios/CacheVideoExample/AppDelegate.mm:
--------------------------------------------------------------------------------
1 | #import "AppDelegate.h"
2 |
3 | #import
4 |
5 | @implementation AppDelegate
6 |
7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
8 | {
9 | self.moduleName = @"CacheVideoExample";
10 | // You can add your custom initial props in the dictionary below.
11 | // They will be passed down to the ViewController used by React Native.
12 | self.initialProps = @{};
13 |
14 | return [super application:application didFinishLaunchingWithOptions:launchOptions];
15 | }
16 |
17 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
18 | {
19 | #if DEBUG
20 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
21 | #else
22 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
23 | #endif
24 | }
25 |
26 | @end
27 |
--------------------------------------------------------------------------------
/src/Utils/constants.ts:
--------------------------------------------------------------------------------
1 | //
2 | export const SIGNAL_NOT_DOWNLOAD_ACTION = 0x1;
3 | // this is important to avoid evict item that just download and does not access anytime
4 | // we assume that if item is not access in any time, it will be have second chance to access
5 | export const SECOND_CHANCE_TO_COUNT = 0;
6 | export const KEY_PREFIX = 'react-native-cache-video';
7 |
8 | // application/x-mpegurl
9 | // application/vnd.apple.mpegurl
10 | export const HLS_CONTENT_TYPE = 'application/x-mpegurl';
11 | export const HLS_VIDEO_TYPE = 'video/MP2T';
12 | export const HLS_CACHING_RESTART = 'RNCV_HLS_CACHING_RESTART';
13 | export const QUERY_ORIGIN_PATH = '__hls_origin_url';
14 | export const LOCALHOST = 'http://127.0.0.1';
15 | export const VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'avi', 'wmv'];
16 |
17 | export const THRESH_HOLD_TIMEOUT = 300;
18 | export const MIN_PORT = 49152;
19 | export const MAX_PORT = 65535;
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/scripts/pod-install.cjs:
--------------------------------------------------------------------------------
1 | const child_process = require('child_process');
2 |
3 | module.exports = {
4 | name: 'pod-install',
5 | factory() {
6 | return {
7 | hooks: {
8 | afterAllInstalled(project, options) {
9 | if (process.env.POD_INSTALL === '0') {
10 | return;
11 | }
12 |
13 | if (
14 | options &&
15 | (options.mode === 'update-lockfile' ||
16 | options.mode === 'skip-build')
17 | ) {
18 | return;
19 | }
20 |
21 | const result = child_process.spawnSync(
22 | 'yarn',
23 | ['pod-install', 'example/ios'],
24 | {
25 | cwd: project.cwd,
26 | env: process.env,
27 | stdio: 'inherit',
28 | encoding: 'utf-8',
29 | shell: true,
30 | }
31 | );
32 |
33 | if (result.status !== 0) {
34 | throw new Error('Failed to run pod-install');
35 | }
36 | },
37 | },
38 | };
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | # Pull Request
2 |
3 | ## Description
4 |
5 |
6 |
7 | ## Related Issue
8 |
9 |
10 |
11 | ## Type of Change
12 |
13 | - [ ] Bug Fix
14 | - [ ] New Feature
15 | - [ ] Enhancement
16 | - [ ] Documentation Update
17 | - [ ] Other (please specify)
18 |
19 | ## Checklist
20 |
21 | - [ ] I have read the [contribution guidelines](CONTRIBUTING.md) and followed the process.
22 | - [ ] My code follows the coding standards of this project.
23 | - [ ] I have added unit tests for my changes/ I have integrate in example project.
24 | - [ ] I have updated the documentation accordingly.
25 | - [ ] The code builds and passes all existing tests locally.
26 |
27 | ## Screenshots (if applicable)
28 |
29 |
30 |
31 | ## Additional Notes
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/example/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # XDE
6 | .expo/
7 |
8 | # VSCode
9 | .vscode/
10 | jsconfig.json
11 |
12 | # Xcode
13 | #
14 | build/
15 | *.pbxuser
16 | !default.pbxuser
17 | *.mode1v3
18 | !default.mode1v3
19 | *.mode2v3
20 | !default.mode2v3
21 | *.perspectivev3
22 | !default.perspectivev3
23 | xcuserdata
24 | *.xccheckout
25 | *.moved-aside
26 | DerivedData
27 | *.hmap
28 | *.ipa
29 | *.xcuserstate
30 | project.xcworkspace
31 |
32 | # Android/IJ
33 | #
34 | .classpath
35 | .cxx
36 | .gradle
37 | .idea
38 | .project
39 | .settings
40 | local.properties
41 | android.iml
42 |
43 | # Cocoapods
44 | #
45 | example/ios/Pods
46 |
47 | # Ruby
48 | example/vendor/
49 |
50 | # node.js
51 | #
52 | node_modules/
53 | npm-debug.log
54 | yarn-debug.log
55 | yarn-error.log
56 |
57 | # BUCK
58 | buck-out/
59 | \.buckd/
60 | android/app/libs
61 | android/keystores/debug.keystore
62 |
63 | # Yarn
64 | .yarn/*
65 | !.yarn/patches
66 | !.yarn/plugins
67 | !.yarn/releases
68 | !.yarn/sdks
69 | !.yarn/versions
70 |
71 | # Expo
72 | .expo/
73 |
74 | # Turborepo
75 | .turbo/
76 |
77 | # generated by bob
78 | lib/
79 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md:
--------------------------------------------------------------------------------
1 | # Pull Request
2 |
3 | ## Description
4 |
5 |
6 |
7 | ## Related Issue
8 |
9 |
10 |
11 | ## Type of Change
12 |
13 | - [ ] Bug Fix
14 | - [ ] New Feature
15 | - [ ] Enhancement
16 | - [ ] Documentation Update
17 | - [ ] Other (please specify)
18 |
19 | ## Checklist
20 |
21 | - [ ] I have read the [contribution guidelines](CONTRIBUTING.md) and followed the process.
22 | - [ ] My code follows the coding standards of this project.
23 | - [ ] I have added unit tests for my changes/ I have integrate in example project.
24 | - [ ] I have updated the documentation accordingly.
25 | - [ ] The code builds and passes all existing tests locally.
26 |
27 | ## Screenshots (if applicable)
28 |
29 |
30 |
31 | ## Additional Notes
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 liberty_ng
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/example/ios/CacheVideoExample/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ios-marketing",
45 | "scale" : "1x",
46 | "size" : "1024x1024"
47 | }
48 | ],
49 | "info" : {
50 | "author" : "xcode",
51 | "version" : 1
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/com/cachevideoexample/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.cachevideoexample;
2 |
3 | import com.facebook.react.ReactActivity;
4 | import com.facebook.react.ReactActivityDelegate;
5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
6 | import com.facebook.react.defaults.DefaultReactActivityDelegate;
7 |
8 | public class MainActivity extends ReactActivity {
9 |
10 | /**
11 | * Returns the name of the main component registered from JavaScript. This is used to schedule
12 | * rendering of the component.
13 | */
14 | @Override
15 | protected String getMainComponentName() {
16 | return "CacheVideoExample";
17 | }
18 |
19 | /**
20 | * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
21 | * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
22 | * (aka React 18) with two boolean flags.
23 | */
24 | @Override
25 | protected ReactActivityDelegate createReactActivityDelegate() {
26 | return new DefaultReactActivityDelegate(
27 | this,
28 | getMainComponentName(),
29 | // If you opted-in for the New Architecture, we enable the Fabric Renderer.
30 | DefaultNewArchitectureEntryPoint.getFabricEnabled());
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import {
4 | CacheManagerProvider,
5 | // FreePolicy,
6 | LFUPolicy,
7 | } from 'react-native-cache-video';
8 | import ListVideo from './components/VideoList';
9 | // import SingleVideo from './components/SingleVideo';
10 | // https://res.cloudinary.com/dannykeane/video/upload/sp_full_hd/q_80:qmax_90,ac_none/v1/dk-memoji-dark.m3u8
11 |
12 | export default function App() {
13 | //
14 | // const _freePolicyRef = React.useRef(new FreePolicy());
15 | const lfuPolicyRef = React.useRef(new LFUPolicy(5));
16 |
17 | // return ;
18 |
19 | return (
20 |
21 | {/* */}
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/example/src/components/SingleVideo.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { DeviceEventEmitter, Image } from 'react-native';
3 | import {
4 | HLS_CACHING_RESTART,
5 | useAsyncCache,
6 | useIsForeground,
7 | } from 'react-native-cache-video';
8 | import Video from 'react-native-video';
9 |
10 | function Component({ uri, thumb }: { uri: string; thumb: string }) {
11 | const { setVideoPlayUrlBy, cachedVideoUrl } = useAsyncCache();
12 | const isForeground = useIsForeground();
13 |
14 | React.useEffect(() => {
15 | const listener = DeviceEventEmitter.addListener(HLS_CACHING_RESTART, () => {
16 | setVideoPlayUrlBy(uri);
17 | // setVideoPlayUrlBy(
18 | // 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'
19 | // );
20 | });
21 |
22 | return () => {
23 | listener.remove();
24 | };
25 | }, [setVideoPlayUrlBy, uri, cachedVideoUrl]);
26 |
27 | return cachedVideoUrl ? (
28 |
39 | ) : (
40 |
46 | );
47 | }
48 |
49 | export default Component;
50 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-cache-video-example",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "android": "react-native run-android",
7 | "ios": "react-native run-ios",
8 | "start": "react-native start",
9 | "build:android": "cd android && ./gradlew assembleDebug --no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a",
10 | "build:ios": "cd ios && xcodebuild -workspace CacheVideoExample.xcworkspace -scheme CacheVideoExample -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO"
11 | },
12 | "dependencies": {
13 | "react": "18.2.0",
14 | "react-native": "0.72.17",
15 | "react-native-blob-util": "^0.19.2",
16 | "react-native-url-polyfill": "^2.0.0",
17 | "react-native-video": "^5.2.1"
18 | },
19 | "devDependencies": {
20 | "@babel/core": "^7.20.0",
21 | "@babel/preset-env": "^7.20.0",
22 | "@babel/runtime": "^7.20.0",
23 | "@react-native/metro-config": "^0.72.11",
24 | "@types/react-native-video": "^5.0.18",
25 | "babel-plugin-module-resolver": "^5.0.0",
26 | "metro-react-native-babel-preset": "0.76.8",
27 | "pod-install": "^0.1.0"
28 | },
29 | "engines": {
30 | "node": ">=16"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/example/metro.config.js:
--------------------------------------------------------------------------------
1 | const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
2 | const path = require('path');
3 | const escape = require('escape-string-regexp');
4 | const exclusionList = require('metro-config/src/defaults/exclusionList');
5 | const pak = require('../package.json');
6 |
7 | const root = path.resolve(__dirname, '..');
8 | const modules = Object.keys({ ...pak.peerDependencies });
9 |
10 | /**
11 | * Metro configuration
12 | * https://facebook.github.io/metro/docs/configuration
13 | *
14 | * @type {import('metro-config').MetroConfig}
15 | */
16 | const config = {
17 | watchFolders: [root],
18 |
19 | // We need to make sure that only one version is loaded for peerDependencies
20 | // So we block them at the root, and alias them to the versions in example's node_modules
21 | resolver: {
22 | blacklistRE: exclusionList(
23 | modules.map(
24 | (m) =>
25 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`)
26 | )
27 | ),
28 |
29 | extraNodeModules: modules.reduce((acc, name) => {
30 | acc[name] = path.join(__dirname, 'node_modules', name);
31 | return acc;
32 | }, {}),
33 | },
34 |
35 | transformer: {
36 | getTransformOptions: async () => ({
37 | transform: {
38 | experimentalImportSupport: false,
39 | inlineRequires: true,
40 | },
41 | }),
42 | },
43 | };
44 |
45 | module.exports = mergeConfig(getDefaultConfig(__dirname), config);
46 |
--------------------------------------------------------------------------------
/example/src/components/VideoItem.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { ImageBackground } from 'react-native';
3 | import { useAsyncCache, useIsForeground } from 'react-native-cache-video';
4 | import Video from 'react-native-video';
5 |
6 | function Component(
7 | { uri, thumb }: { uri: string; thumb: string },
8 | ref: React.Ref | undefined
9 | ) {
10 | const { setVideoPlayUrlBy, cachedVideoUrl } = useAsyncCache();
11 | const [shouldDisplay, setDisplay] = React.useState(false);
12 | const isForeground = useIsForeground();
13 |
14 | React.useImperativeHandle(ref, () => ({
15 | setDisplay: (display: boolean) => {
16 | if (display) {
17 | setVideoPlayUrlBy(uri);
18 | } else {
19 | setDisplay(false);
20 | setVideoPlayUrlBy(undefined);
21 | }
22 | },
23 | }));
24 |
25 | return (
26 |
32 | {cachedVideoUrl && (
33 |
47 | );
48 | }
49 |
50 | export default React.forwardRef(Component);
51 |
--------------------------------------------------------------------------------
/ios/GCDWebServer/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012-2014, Pierre-Olivier Latour
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 | * Redistributions of source code must retain the above copyright
7 | notice, this list of conditions and the following disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright
9 | notice, this list of conditions and the following disclaimer in the
10 | documentation and/or other materials provided with the distribution.
11 | * The name of Pierre-Olivier Latour may not be used to endorse
12 | or promote products derived from this software without specific
13 | prior written permission.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | DISCLAIMED. IN NO EVENT SHALL PIERRE-OLIVIER LATOUR BE LIABLE FOR ANY
19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
--------------------------------------------------------------------------------
/android/src/main/java/com/cachevideo/CacheVideoHttpProxyPackage.java:
--------------------------------------------------------------------------------
1 | package com.cachevideo;
2 |
3 | import android.util.Log;
4 |
5 | import androidx.annotation.Nullable;
6 |
7 | import com.facebook.react.bridge.NativeModule;
8 | import com.facebook.react.bridge.ReactApplicationContext;
9 | import com.facebook.react.module.model.ReactModuleInfo;
10 | import com.facebook.react.module.model.ReactModuleInfoProvider;
11 | import com.facebook.react.TurboReactPackage;
12 |
13 | import java.util.HashMap;
14 | import java.util.Map;
15 |
16 | public class CacheVideoHttpProxyPackage extends TurboReactPackage {
17 |
18 | @Nullable
19 | @Override
20 | public NativeModule getModule(String name, ReactApplicationContext reactContext) {
21 | if (name.equals(CacheVideoHttpProxyModule.NAME)) {
22 | Log.d(CacheVideoHttpProxyModule.NAME, reactContext.toString());
23 | return new CacheVideoHttpProxyModule(reactContext);
24 | } else {
25 | return null;
26 | }
27 | }
28 |
29 | @Override
30 | public ReactModuleInfoProvider getReactModuleInfoProvider() {
31 | return () -> {
32 | final Map moduleInfos = new HashMap<>();
33 | boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
34 | moduleInfos.put(
35 | CacheVideoHttpProxyModule.NAME,
36 | new ReactModuleInfo(
37 | CacheVideoHttpProxyModule.NAME,
38 | CacheVideoHttpProxyModule.NAME,
39 | false, // canOverrideExistingModule
40 | false, // needsEagerInit
41 | true, // hasConstants
42 | false, // isCxxModule
43 | isTurboModule // isTurboModule
44 | ));
45 | return moduleInfos;
46 | };
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/react-native-cache-video.podspec:
--------------------------------------------------------------------------------
1 | require "json"
2 |
3 | package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4 | folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
5 |
6 | Pod::Spec.new do |s|
7 | s.name = "react-native-cache-video"
8 | s.version = package["version"]
9 | s.summary = package["description"]
10 | s.homepage = package["homepage"]
11 | s.license = package["license"]
12 | s.authors = package["author"]
13 |
14 | s.platforms = { :ios => "12.4" }
15 | s.source = { :git => "https://github.com/nguyenvanphituoc/react-native-cache-video.git", :tag => "#{s.version}" }
16 |
17 | s.source_files = "ios/**/*.{h,m,mm}"
18 |
19 | # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
20 | # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
21 | if respond_to?(:install_modules_dependencies, true)
22 | install_modules_dependencies(s)
23 | else
24 | s.dependency "React-Core"
25 | # Don't install the dependencies when we run `pod install` in the old architecture.
26 | if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
27 | s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
28 | s.pod_target_xcconfig = {
29 | "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
30 | "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
31 | "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
32 | }
33 | s.dependency "React-Codegen"
34 | s.dependency "RCT-Folly"
35 | s.dependency "RCTRequired"
36 | s.dependency "RCTTypeSafety"
37 | s.dependency "ReactCommon/turbomodule/core"
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/example/ios/CacheVideoExample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LSApplicationCategoryType
6 |
7 | CFBundleDevelopmentRegion
8 | en
9 | CFBundleDisplayName
10 | CacheVideoExample
11 | CFBundleExecutable
12 | $(EXECUTABLE_NAME)
13 | CFBundleIdentifier
14 | $(PRODUCT_BUNDLE_IDENTIFIER)
15 | CFBundleInfoDictionaryVersion
16 | 6.0
17 | CFBundleName
18 | $(PRODUCT_NAME)
19 | CFBundlePackageType
20 | APPL
21 | CFBundleShortVersionString
22 | $(MARKETING_VERSION)
23 | CFBundleSignature
24 | ????
25 | CFBundleVersion
26 | $(CURRENT_PROJECT_VERSION)
27 | LSRequiresIPhoneOS
28 |
29 | NSAppTransportSecurity
30 |
31 | NSAllowsArbitraryLoads
32 |
33 | NSExceptionDomains
34 |
35 | localhost
36 |
37 | NSExceptionAllowsInsecureHTTPLoads
38 |
39 |
40 |
41 |
42 | NSLocationWhenInUseUsageDescription
43 |
44 | UILaunchStoryboardName
45 | LaunchScreen
46 | UIRequiredDeviceCapabilities
47 |
48 | armv7
49 |
50 | UISupportedInterfaceOrientations
51 |
52 | UIInterfaceOrientationPortrait
53 | UIInterfaceOrientationLandscapeLeft
54 | UIInterfaceOrientationLandscapeRight
55 |
56 | UIViewControllerBasedStatusBarAppearance
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/drawable/rn_edit_text_material.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
21 |
22 |
23 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/example/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
24 | # Automatically convert third-party libraries to use AndroidX
25 | android.enableJetifier=true
26 |
27 | # Version of flipper SDK to use with React Native
28 | FLIPPER_VERSION=0.182.0
29 |
30 | # Use this property to specify which architecture you want to build.
31 | # You can also override it from the CLI using
32 | # ./gradlew -PreactNativeArchitectures=x86_64
33 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
34 |
35 | # Use this property to enable support to the new architecture.
36 | # This will allow you to use TurboModules and the Fabric render in
37 | # your application. You should enable this flag either if you want
38 | # to write custom TurboModules/Fabric components OR use libraries that
39 | # are providing them.
40 | newArchEnabled=false
41 |
42 | # Use this property to enable or disable the Hermes JS engine.
43 | # If set to false, you will be using JSC instead.
44 | hermesEnabled=true
45 |
--------------------------------------------------------------------------------
/src/Libs/session.ts:
--------------------------------------------------------------------------------
1 | import RNFetchBlob from 'react-native-blob-util';
2 |
3 | import type {
4 | FetchBlobResponse,
5 | StatefulPromise,
6 | } from 'react-native-blob-util';
7 |
8 | import type {
9 | SessionTaskInterface,
10 | SessionTaskOptionsType,
11 | } from '../types/type';
12 | import { KEY_PREFIX } from '../Utils/constants';
13 |
14 | export * from 'react-native-blob-util';
15 |
16 | export class SimpleSessionProvider implements SessionTaskInterface {
17 | // current caching m3u8 playlist
18 | // any are session task object
19 | private downloadingList: {
20 | [key in string]?: StatefulPromise;
21 | } = {};
22 |
23 | dataTask = (
24 | url: string,
25 | options: SessionTaskOptionsType,
26 | callback?: (data: any, res: any, error?: Error) => void
27 | ): StatefulPromise => {
28 | const downloadTask = RNFetchBlob.config({
29 | session: KEY_PREFIX,
30 | ...options,
31 | }).fetch('GET', url, {
32 | 'RNFB-Response': 'base64',
33 | ...options.headers,
34 | });
35 | // mark it as downloading
36 | this.downloadingList[url] = downloadTask;
37 | // listen response download
38 | downloadTask
39 | .then((res) => {
40 | // res.respInfo?.headers && console.log(res.respInfo?.headers);
41 | callback && callback(res.data, res, undefined);
42 | })
43 | .catch((error) => {
44 | callback && callback(null, null, error);
45 | })
46 | .finally(() => {
47 | delete this.downloadingList[url];
48 | });
49 | //
50 | return downloadTask;
51 | };
52 |
53 | cancelTask = (url: string) => {
54 | const downloadTask = this.downloadingList[url];
55 | if (!downloadTask) {
56 | return;
57 | }
58 |
59 | downloadTask.cancel();
60 |
61 | delete this.downloadingList[url];
62 | };
63 |
64 | cancelAllTask = () => {
65 | Object.entries(this.downloadingList).forEach(([url, downloadTask]) => {
66 | url && downloadTask?.cancel();
67 | });
68 |
69 | this.downloadingList = {};
70 | };
71 | }
72 |
--------------------------------------------------------------------------------
/example/src/components/VideoList.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import { FlatList, Text, Dimensions, View } from 'react-native';
3 | import VideoItem from './VideoItem'; // Assuming VideoItem is in the same directory
4 | // import { mediaJSON } from '../data/videos';
5 | import { mediaJSON } from '../data/streams';
6 |
7 | const dimension = Dimensions.get('window');
8 |
9 | export default function VideoList() {
10 | const videos = mediaJSON?.categories?.[0]?.videos;
11 | const videoRefs = useRef([]);
12 | const currentDisplayIndex = useRef(-1);
13 |
14 | return (
15 | item.title}
21 | getItemLayout={(_data, index) => ({
22 | length: dimension.height,
23 | offset: dimension.height * index,
24 | index,
25 | })}
26 | onMomentumScrollEnd={(event) => {
27 | const pageIndex = Math.round(
28 | event.nativeEvent.contentOffset.y / dimension.height
29 | );
30 | const prevIndex = currentDisplayIndex.current;
31 |
32 | if (pageIndex > -1) {
33 | videoRefs.current[pageIndex]?.setDisplay(true);
34 | currentDisplayIndex.current = pageIndex;
35 | videoRefs.current[prevIndex]?.setDisplay(false);
36 | }
37 | // preparing for list
38 | }}
39 | renderItem={({ item, index }) => (
40 |
41 | (videoRefs.current[index] = ref)}
43 | uri={item.sources[0] ?? ''}
44 | thumb={item.thumb}
45 | />
46 | {/* */}
47 |
55 | {item.title}
56 |
57 |
58 | )}
59 | />
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/example/src/hooks/useVideoInBackground.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useRef } from 'react';
2 | import { useAsyncCache } from 'react-native-cache-video';
3 |
4 | import Video from 'react-native-video';
5 |
6 | export function useVideoInBackGround() {
7 | const playlistRef = useRef([]);
8 | const lastVideoPlayingRef = useRef<{
9 | url: string | undefined;
10 | index: number | undefined;
11 | }>();
12 | const { setVideoPlayUrlBy, cachedVideoUrl } = useAsyncCache();
13 |
14 | const setPlayListVideo = useCallback(
15 | (urls: string[]) => {
16 | playlistRef.current = urls;
17 | // get first video
18 | if (playlistRef.current?.length) {
19 | const firstVideo = playlistRef.current[0];
20 | if (firstVideo) {
21 | lastVideoPlayingRef.current = {
22 | url: firstVideo,
23 | index: 0,
24 | };
25 | setVideoPlayUrlBy(firstVideo);
26 | }
27 | }
28 | },
29 | [setVideoPlayUrlBy]
30 | );
31 |
32 | const BackgroundVideo = useCallback(() => {
33 | return (
34 |