getTurboModule(
27 | const std::string &name,
28 | const JavaTurboModule::InitParams ¶ms) override;
29 |
30 | /**
31 | * Test-only method. Allows user to verify whether a TurboModule can be
32 | * created by instances of this class.
33 | */
34 | bool canCreateTurboModule(const std::string &name);
35 | };
36 |
37 | } // namespace react
38 | } // namespace facebook
39 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/com/example/reactnativertmp/newarchitecture/components/MainComponentsRegistry.java:
--------------------------------------------------------------------------------
1 | package com.example.reactnativertmp.newarchitecture.components;
2 |
3 | import com.facebook.jni.HybridData;
4 | import com.facebook.proguard.annotations.DoNotStrip;
5 | import com.facebook.react.fabric.ComponentFactory;
6 | import com.facebook.soloader.SoLoader;
7 |
8 | /**
9 | * Class responsible to load the custom Fabric Components. This class has native methods and needs a
10 | * corresponding C++ implementation/header file to work correctly (already placed inside the jni/
11 | * folder for you).
12 | *
13 | * Please note that this class is used ONLY if you opt-in for the New Architecture (see the
14 | * `newArchEnabled` property). Is ignored otherwise.
15 | */
16 | @DoNotStrip
17 | public class MainComponentsRegistry {
18 | static {
19 | SoLoader.loadLibrary("fabricjni");
20 | }
21 |
22 | @DoNotStrip private final HybridData mHybridData;
23 |
24 | @DoNotStrip
25 | private native HybridData initHybrid(ComponentFactory componentFactory);
26 |
27 | @DoNotStrip
28 | private MainComponentsRegistry(ComponentFactory componentFactory) {
29 | mHybridData = initHybrid(componentFactory);
30 | }
31 |
32 | @DoNotStrip
33 | public static MainComponentsRegistry register(ComponentFactory componentFactory) {
34 | return new MainComponentsRegistry(componentFactory);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/example/src/hooks/usePermissions.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { PermissionsAndroid, Platform } from 'react-native';
3 |
4 | const CAMERA_PERMISSION = PermissionsAndroid.PERMISSIONS.CAMERA;
5 | const AUDIO_PERMISSION = PermissionsAndroid.PERMISSIONS.RECORD_AUDIO;
6 | const BLUETOOTH_PERMISSION = PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT;
7 |
8 | function usePermissions() {
9 | const [permissionGranted, setPermissionGranted] = useState(false);
10 |
11 | useEffect(() => {
12 | getPermissions();
13 | }, []);
14 |
15 | async function getPermissions() {
16 | if (Platform.OS !== 'android') {
17 | setPermissionGranted(true);
18 | return;
19 | }
20 |
21 | const cameraPermission = await PermissionsAndroid.check(CAMERA_PERMISSION);
22 | const audioPermission = await PermissionsAndroid.check(AUDIO_PERMISSION);
23 | const bluetoothPermission = await PermissionsAndroid.check(
24 | BLUETOOTH_PERMISSION
25 | );
26 |
27 | if (cameraPermission && audioPermission && bluetoothPermission) {
28 | return setPermissionGranted(true);
29 | }
30 |
31 | const hasPermissions = await PermissionsAndroid.requestMultiple([
32 | CAMERA_PERMISSION,
33 | AUDIO_PERMISSION,
34 | BLUETOOTH_PERMISSION,
35 | ]);
36 |
37 | if (hasPermissions) {
38 | return setPermissionGranted(true);
39 | }
40 | }
41 |
42 | return { permissionGranted };
43 | }
44 |
45 | export default usePermissions;
46 |
--------------------------------------------------------------------------------
/src/Component.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | NativeSyntheticEvent,
3 | requireNativeComponent,
4 | ViewStyle,
5 | } from 'react-native';
6 | import type { StreamState, BluetoothDeviceStatuses } from './types';
7 |
8 | type RTMPData = { data: T };
9 |
10 | export type ConnectionFailedType = NativeSyntheticEvent>;
11 | export type ConnectionStartedType = NativeSyntheticEvent>;
12 | export type ConnectionSuccessType = NativeSyntheticEvent>;
13 | export type DisconnectType = NativeSyntheticEvent>;
14 | export type NewBitrateReceivedType = NativeSyntheticEvent>;
15 | export type StreamStateChangedType = NativeSyntheticEvent<
16 | RTMPData
17 | >;
18 | export type BluetoothDeviceStatusChangedType = NativeSyntheticEvent<
19 | RTMPData
20 | >;
21 | export interface NativeRTMPPublisherProps {
22 | style?: ViewStyle;
23 | streamURL: string;
24 | streamName: string;
25 | onConnectionFailed?: (e: ConnectionFailedType) => void;
26 | onConnectionStarted?: (e: ConnectionStartedType) => void;
27 | onConnectionSuccess?: (e: ConnectionSuccessType) => void;
28 | onDisconnect?: (e: DisconnectType) => void;
29 | onNewBitrateReceived?: (e: NewBitrateReceivedType) => void;
30 | onStreamStateChanged?: (e: StreamStateChangedType) => void;
31 | onBluetoothDeviceStatusChanged?: (
32 | e: BluetoothDeviceStatusChangedType
33 | ) => void;
34 | }
35 | export default requireNativeComponent(
36 | 'RTMPPublisher'
37 | );
38 |
--------------------------------------------------------------------------------
/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 | android.useAndroidX=true
21 | android.enableJetifier=true
22 | FLIPPER_VERSION=0.125.0
23 | # Use this property to specify which architecture you want to build.
24 | # You can also override it from the CLI using
25 | # ./gradlew -PreactNativeArchitectures=x86_64
26 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
27 | # Use this property to enable support to the new architecture.
28 | # This will allow you to use TurboModules and the Fabric render in
29 | # your application. You should enable this flag either if you want
30 | # to write custom TurboModules/Fabric components OR use libraries that
31 | # are providing them.
32 | newArchEnabled=false
--------------------------------------------------------------------------------
/example/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.cpp:
--------------------------------------------------------------------------------
1 | #include "MainApplicationTurboModuleManagerDelegate.h"
2 | #include "MainApplicationModuleProvider.h"
3 |
4 | namespace facebook {
5 | namespace react {
6 |
7 | jni::local_ref
8 | MainApplicationTurboModuleManagerDelegate::initHybrid(
9 | jni::alias_ref) {
10 | return makeCxxInstance();
11 | }
12 |
13 | void MainApplicationTurboModuleManagerDelegate::registerNatives() {
14 | registerHybrid({
15 | makeNativeMethod(
16 | "initHybrid", MainApplicationTurboModuleManagerDelegate::initHybrid),
17 | makeNativeMethod(
18 | "canCreateTurboModule",
19 | MainApplicationTurboModuleManagerDelegate::canCreateTurboModule),
20 | });
21 | }
22 |
23 | std::shared_ptr
24 | MainApplicationTurboModuleManagerDelegate::getTurboModule(
25 | const std::string &name,
26 | const std::shared_ptr &jsInvoker) {
27 | // Not implemented yet: provide pure-C++ NativeModules here.
28 | return nullptr;
29 | }
30 |
31 | std::shared_ptr
32 | MainApplicationTurboModuleManagerDelegate::getTurboModule(
33 | const std::string &name,
34 | const JavaTurboModule::InitParams ¶ms) {
35 | return MainApplicationModuleProvider(name, params);
36 | }
37 |
38 | bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule(
39 | const std::string &name) {
40 | return getTurboModule(name, nullptr) != nullptr ||
41 | getTurboModule(name, {.moduleName = name}) != nullptr;
42 | }
43 |
44 | } // namespace react
45 | } // namespace facebook
46 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | if (project == rootProject) {
3 | repositories {
4 | google()
5 | mavenCentral()
6 | jcenter()
7 | }
8 |
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:3.5.3'
11 | }
12 | }
13 | }
14 |
15 | apply plugin: 'com.android.library'
16 |
17 | def safeExtGet(prop, fallback) {
18 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
19 | }
20 |
21 | android {
22 | compileSdkVersion safeExtGet('Rtmp_compileSdkVersion', 31)
23 | defaultConfig {
24 | minSdkVersion safeExtGet('Rtmp_minSdkVersion', 21)
25 | targetSdkVersion safeExtGet('Rtmp_targetSdkVersion', 31)
26 | versionCode 1
27 | versionName "1.0"
28 |
29 | }
30 |
31 | buildTypes {
32 | release {
33 | minifyEnabled false
34 | }
35 | }
36 | lintOptions {
37 | disable 'GradleCompatible'
38 | }
39 | compileOptions {
40 | sourceCompatibility JavaVersion.VERSION_1_8
41 | targetCompatibility JavaVersion.VERSION_1_8
42 | }
43 | }
44 |
45 | repositories {
46 | mavenLocal()
47 | maven {
48 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
49 | url("$rootDir/../node_modules/react-native/android")
50 | }
51 | google()
52 | mavenCentral()
53 | jcenter()
54 | maven { url 'https://www.jitpack.io' }
55 | }
56 |
57 | dependencies {
58 | //noinspection GradleDynamicVersion
59 | implementation fileTree(dir: 'libs', include: ['*.jar'])
60 | implementation 'com.github.pedroSG94.rtmp-rtsp-stream-client-java:rtplibrary:2.2.2'
61 | implementation "com.facebook.react:react-native:+" // From node_modules
62 | }
63 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/com/example/reactnativertmp/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.example.reactnativertmp;
2 |
3 | import com.facebook.react.ReactActivity;
4 | import com.facebook.react.ReactActivityDelegate;
5 | import com.facebook.react.ReactRootView;
6 |
7 | public class MainActivity extends ReactActivity {
8 |
9 | /**
10 | * Returns the name of the main component registered from JavaScript. This is used to schedule
11 | * rendering of the component.
12 | */
13 | @Override
14 | protected String getMainComponentName() {
15 | return "RtmpExample";
16 | }
17 |
18 | /**
19 | * Returns the instance of the {@link ReactActivityDelegate}. There the RootView is created and
20 | * you can specify the renderer you wish to use - the new renderer (Fabric) or the old renderer
21 | * (Paper).
22 | */
23 | @Override
24 | protected ReactActivityDelegate createReactActivityDelegate() {
25 | return new MainActivityDelegate(this, getMainComponentName());
26 | }
27 | public static class MainActivityDelegate extends ReactActivityDelegate {
28 | public MainActivityDelegate(ReactActivity activity, String mainComponentName) {
29 | super(activity, mainComponentName);
30 | }
31 | @Override
32 | protected ReactRootView createRootView() {
33 | ReactRootView reactRootView = new ReactRootView(getContext());
34 | // If you opted-in for the New Architecture, we enable the Fabric Renderer.
35 | reactRootView.setIsFabric(BuildConfig.IS_NEW_ARCHITECTURE_ENABLED);
36 | return reactRootView;
37 | }
38 | @Override
39 | protected boolean isConcurrentRootEnabled() {
40 | // If you opted-in for the New Architecture, we enable Concurrent Root (i.e. React 18).
41 | // More on this on https://reactjs.org/blog/2022/03/29/react-v18.html
42 | return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: File a bug report
3 | body:
4 | - type: textarea
5 | id: describe-the-bug
6 | attributes:
7 | label: Describe the bug
8 | description: A clear and concise description of what the bug is.
9 | validations:
10 | required: true
11 | - type: textarea
12 | id: to-reproduce
13 | attributes:
14 | label: To Reproduce
15 | description: Steps to reproduce the behavior
16 | placeholder: |
17 | 1. Go to '...'
18 | 2. Click on '....'
19 | 3. Scroll down to '....'
20 | 4. See error
21 | validations:
22 | required: true
23 | - type: textarea
24 | id: expected-behavior
25 | attributes:
26 | label: Expected behavior
27 | description: Steps to reproduce the behavior
28 | validations:
29 | required: true
30 | - type: textarea
31 | id: version
32 | attributes:
33 | label: Version
34 | description: What version of our software are you running?
35 | validations:
36 | required: true
37 | - type: textarea
38 | id: smartphone-info
39 | attributes:
40 | label: Smartphone info.
41 | description: please complete the following information
42 | placeholder: |
43 | - Device: [e.g. iPhone6]
44 | - OS: [e.g. iOS8.1]
45 | - type: textarea
46 | id: addditional-context
47 | attributes:
48 | label: Additional context
49 | description: Add any other context about the problem here.
50 | - type: textarea
51 | id: screenshot
52 | attributes:
53 | label: Screenshots
54 | description: If applicable, add screenshots to help explain your problem.
55 | - type: textarea
56 | id: logs
57 | attributes:
58 | label: Relevant log output
59 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
60 | render: shell
--------------------------------------------------------------------------------
/example/ios/RtmpExample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | Rtmp Example
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 1
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 | NSExceptionDomains
30 |
31 | localhost
32 |
33 | NSExceptionAllowsInsecureHTTPLoads
34 |
35 |
36 |
37 |
38 | NSLocationWhenInUseUsageDescription
39 |
40 | UILaunchStoryboardName
41 | LaunchScreen
42 | UIRequiredDeviceCapabilities
43 |
44 | armv7
45 |
46 | UISupportedInterfaceOrientations
47 |
48 | UIInterfaceOrientationPortrait
49 | UIInterfaceOrientationLandscapeLeft
50 | UIInterfaceOrientationLandscapeRight
51 |
52 | NSCameraUsageDescription
53 | Publishing the video
54 | NSMicrophoneUsageDescription
55 | Publishing the audio
56 | UIViewControllerBasedStatusBarAppearance
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/example/android/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext {
5 | buildToolsVersion = "31.0.0"
6 | minSdkVersion = 21
7 | compileSdkVersion = 31
8 | targetSdkVersion = 31
9 | if (System.properties['os.arch'] == "aarch64") {
10 | // For M1 Users we need to use the NDK 24 which added support for aarch64
11 | ndkVersion = "24.0.8215888"
12 | } else {
13 | // Otherwise we default to the side-by-side NDK version from AGP.
14 | ndkVersion = "21.4.7075529"
15 | }
16 | }
17 | repositories {
18 | google()
19 | mavenCentral()
20 | // jcenter()
21 | }
22 | dependencies {
23 | classpath("com.android.tools.build:gradle:7.2.1")
24 | classpath("com.facebook.react:react-native-gradle-plugin")
25 | classpath("de.undercouch:gradle-download-task:5.0.1")
26 | // NOTE: Do not place your application dependencies here; they belong
27 | // in the individual module build.gradle files
28 | }
29 | }
30 |
31 | allprojects {
32 | repositories {
33 | // mavenLocal()
34 | maven {
35 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
36 | url("$rootDir/../node_modules/react-native/android")
37 | }
38 | maven {
39 | // Android JSC is installed from npm
40 | url("$rootDir/../node_modules/jsc-android/dist")
41 | }
42 | mavenCentral {
43 | // We don't want to fetch react-native from Maven Central as there are
44 | // older versions over there.
45 | content {
46 | excludeGroup "com.facebook.react"
47 | }
48 | }
49 | google()
50 | mavenCentral()
51 | // jcenter()
52 | maven { url 'https://www.jitpack.io' }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/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/ios/Podfile:
--------------------------------------------------------------------------------
1 | require_relative '../node_modules/react-native/scripts/react_native_pods'
2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
3 |
4 | platform :ios, '12.4'
5 | install! 'cocoapods', :deterministic_uuids => false
6 |
7 | target 'RtmpExample' do
8 | config = use_native_modules!
9 |
10 | # Flags change depending on the env values.
11 | flags = get_default_flags()
12 | use_react_native!(
13 | :path => config[:reactNativePath],
14 | # Hermes is now enabled by default. Disable by setting this flag to false.
15 | # Upcoming versions of React Native may rely on get_default_flags(), but
16 | # we make it explicit here to aid in the React Native upgrade process.
17 | :hermes_enabled => true,
18 | :fabric_enabled => flags[:fabric_enabled],
19 | # Enables Flipper.
20 | #
21 | # Note that if you have use_frameworks! enabled, Flipper will not work and
22 | # you should disable the next line.
23 | :flipper_configuration => FlipperConfiguration.enabled,
24 | # An absolute path to your application root.
25 | :app_path => "#{Pod::Config.instance.installation_root}/.."
26 | )
27 |
28 | pod 'react-native-rtmp-publisher', :path => '../..'
29 |
30 | # Enables Flipper.
31 | #
32 | # Note that if you have use_frameworks! enabled, Flipper will not work and
33 | # you should disable these next few lines.
34 | # use_flipper!({ 'Flipper' => '0.80.0' })
35 | # post_install do |installer|
36 | # flipper_post_install(installer)
37 | # end
38 |
39 | post_install do |installer|
40 | react_native_post_install(installer)
41 | installer.pods_project.build_configurations.each do |config| config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
42 | end
43 |
44 | react_native_post_install(
45 | installer,
46 | # Set `mac_catalyst_enabled` to `true` in order to apply patches
47 | # necessary for Mac Catalyst builds
48 | :mac_catalyst_enabled => false
49 | )
50 | __apply_Xcode_12_5_M1_post_install_workaround(installer)
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/android/src/main/java/com/reactnativertmppublisher/modules/ConnectionChecker.java:
--------------------------------------------------------------------------------
1 | package com.reactnativertmppublisher.modules;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import com.pedro.rtmp.utils.ConnectCheckerRtmp;
6 | import com.reactnativertmppublisher.interfaces.ConnectionListener;
7 |
8 | import java.util.ArrayList;
9 | import java.util.List;
10 |
11 | public class ConnectionChecker implements ConnectCheckerRtmp {
12 | private final List listeners = new ArrayList<>();
13 |
14 | public void addListener(ConnectionListener listener) {
15 | listeners.add(listener);
16 | }
17 |
18 | @Override
19 | public void onAuthErrorRtmp() {
20 | for (ConnectionListener l : listeners) {
21 | l.onChange("onAuthError", null);
22 | }
23 | }
24 |
25 | @Override
26 | public void onAuthSuccessRtmp() {
27 | for (ConnectionListener l : listeners) {
28 | l.onChange("onAuthSuccess", null);
29 | }
30 | }
31 |
32 | // TODO: Parameters will be send after onChange method updated
33 | @Override
34 | public void onConnectionFailedRtmp(@NonNull String s) {
35 | for (ConnectionListener l : listeners) {
36 | l.onChange("onConnectionFailed", s);
37 | }
38 | }
39 |
40 | // TODO: Parameters will be send after onChange method updated
41 | @Override
42 | public void onConnectionStartedRtmp(@NonNull String s) {
43 | for (ConnectionListener l : listeners) {
44 | l.onChange("onConnectionStarted", s);
45 | }
46 | }
47 |
48 | @Override
49 | public void onConnectionSuccessRtmp() {
50 | for (ConnectionListener l : listeners) {
51 | l.onChange("onConnectionSuccess", null);
52 | }
53 | }
54 |
55 | @Override
56 | public void onDisconnectRtmp() {
57 | for (ConnectionListener l : listeners) {
58 | l.onChange("onDisconnect", null);
59 | }
60 | }
61 |
62 | // TODO: Parameters will be send after onChange method updated
63 | @Override
64 | public void onNewBitrateRtmp(long b) {
65 | for (ConnectionListener l : listeners) {
66 | l.onChange("onNewBitrateReceived", b);
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/com/example/reactnativertmp/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate.java:
--------------------------------------------------------------------------------
1 | package com.example.reactnativertmp.newarchitecture.modules;
2 |
3 | import com.facebook.jni.HybridData;
4 | import com.facebook.react.ReactPackage;
5 | import com.facebook.react.ReactPackageTurboModuleManagerDelegate;
6 | import com.facebook.react.bridge.ReactApplicationContext;
7 | import com.facebook.soloader.SoLoader;
8 | import java.util.List;
9 |
10 | /**
11 | * Class responsible to load the TurboModules. This class has native methods and needs a
12 | * corresponding C++ implementation/header file to work correctly (already placed inside the jni/
13 | * folder for you).
14 | *
15 | * Please note that this class is used ONLY if you opt-in for the New Architecture (see the
16 | * `newArchEnabled` property). Is ignored otherwise.
17 | */
18 | public class MainApplicationTurboModuleManagerDelegate
19 | extends ReactPackageTurboModuleManagerDelegate {
20 |
21 | private static volatile boolean sIsSoLibraryLoaded;
22 |
23 | protected MainApplicationTurboModuleManagerDelegate(
24 | ReactApplicationContext reactApplicationContext, List packages) {
25 | super(reactApplicationContext, packages);
26 | }
27 |
28 | protected native HybridData initHybrid();
29 |
30 | native boolean canCreateTurboModule(String moduleName);
31 |
32 | public static class Builder extends ReactPackageTurboModuleManagerDelegate.Builder {
33 | protected MainApplicationTurboModuleManagerDelegate build(
34 | ReactApplicationContext context, List packages) {
35 | return new MainApplicationTurboModuleManagerDelegate(context, packages);
36 | }
37 | }
38 |
39 | @Override
40 | protected synchronized void maybeLoadOtherSoLibraries() {
41 | if (!sIsSoLibraryLoaded) {
42 | // If you change the name of your application .so file in the Android.mk file,
43 | // make sure you update the name here as well.
44 | SoLoader.loadLibrary("reactnativertmp_appmodules");
45 | sIsSoLibraryLoaded = true;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { ViewStyle } from 'react-native';
2 |
3 | export interface RTMPPublisherRefProps {
4 | /**
5 | * Starts stream operation
6 | */
7 | startStream: () => Promise;
8 | /**
9 | * Stops stream operation
10 | */
11 | stopStream: () => Promise;
12 | /**
13 | * Checks stream status
14 | */
15 | isStreaming: () => Promise;
16 | /**
17 | * Checks if camera on mount
18 | */
19 | isCameraOnPreview: () => Promise;
20 | /**
21 | * Gets settled publish url
22 | */
23 | getPublishURL: () => Promise;
24 | /**
25 | * Checks congestion status
26 | */
27 | hasCongestion: () => Promise;
28 | /**
29 | * Checks audio status
30 | */
31 | isAudioPrepared: () => Promise;
32 | /**
33 | * Checks video status
34 | */
35 | isVideoPrepared: () => Promise;
36 | /**
37 | * Checks if mic closed
38 | */
39 | isMuted: () => Promise;
40 | /**
41 | * Mutes the mic
42 | */
43 | mute: () => Promise;
44 | /**
45 | * Unmutes the mic
46 | */
47 | unmute: () => Promise;
48 | /**
49 | * Switches the camera
50 | */
51 | switchCamera: () => Promise;
52 | /**
53 | * Toggles the flash
54 | */
55 | toggleFlash: () => Promise;
56 | /**
57 | * Sets the audio input (microphone type)
58 | */
59 | setAudioInput: (audioInput: AudioInputType) => Promise;
60 | }
61 |
62 | export interface RTMPPublisherProps {
63 | style?: ViewStyle;
64 | streamURL: string;
65 | streamName: string;
66 | onConnectionFailed?: (e: null) => void;
67 | onConnectionStarted?: (e: null) => void;
68 | onConnectionSuccess?: (e: null) => void;
69 | onDisconnect?: (e: null) => void;
70 | onNewBitrateReceived?: (e: number) => void;
71 | onStreamStateChanged?: (e: StreamState) => void;
72 | }
73 | export type StreamStatus =
74 | | 'CONNECTING'
75 | | 'CONNECTED'
76 | | 'DISCONNECTED'
77 | | 'FAILED';
78 |
79 | export enum StreamState {
80 | CONNECTING = 'CONNECTING',
81 | CONNECTED = 'CONNECTED',
82 | DISCONNECTED = 'DISCONNECTED',
83 | FAILED = 'FAILED',
84 | }
85 | export enum BluetoothDeviceStatuses {
86 | CONNECTING = 'CONNECTING',
87 | CONNECTED = 'CONNECTED',
88 | DISCONNECTED = 'DISCONNECTED',
89 | }
90 |
91 | export enum AudioInputType {
92 | BLUETOOTH_HEADSET = 0,
93 | SPEAKER = 1,
94 | WIRED_HEADSET = 2,
95 | }
96 |
--------------------------------------------------------------------------------
/example/src/components/MicrophoneSelectModal/MicrophoneSelectModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { View, Text, Modal, TouchableOpacity, Platform } from 'react-native';
3 | import { AudioInputType } from 'react-native-rtmp-publisher';
4 | import Button from '../Button';
5 |
6 | import styles, { itemStyles } from './MicrophoneSelectModal.styles';
7 |
8 | export interface ButtonProps {
9 | visible: boolean;
10 | onClose: () => void;
11 | onSelect: (a: AudioInputType) => void;
12 | }
13 |
14 | const initialMicrophoneValues = [
15 | {
16 | key: AudioInputType.SPEAKER,
17 | title: 'Speaker',
18 | },
19 | {
20 | key: AudioInputType.BLUETOOTH_HEADSET,
21 | title: 'Bluetooth Headset',
22 | },
23 | {
24 | key: AudioInputType.WIRED_HEADSET,
25 | title: 'Wired Headset',
26 | },
27 | ];
28 |
29 | const MicrophoneSelectModal = ({ visible, onSelect, onClose }: ButtonProps) => {
30 | const [selectedMicrophoneKey, setSelectedMicrophoneKey] =
31 | useState();
32 |
33 | const handleSelect = (value: AudioInputType) => {
34 | onSelect(value);
35 |
36 | setSelectedMicrophoneKey(value);
37 | };
38 |
39 | return (
40 |
46 |
47 |
48 | {initialMicrophoneValues.map((value) => {
49 | if (
50 | Platform.OS === 'android' &&
51 | value.key === AudioInputType.WIRED_HEADSET
52 | ) {
53 | return;
54 | }
55 |
56 | const status =
57 | selectedMicrophoneKey === value.key ? 'selected' : 'not_selected';
58 |
59 | return (
60 | handleSelect(value.key)}
64 | >
65 | {value.title}
66 |
67 | );
68 | })}
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | );
77 | };
78 |
79 | export default MicrophoneSelectModal;
80 |
--------------------------------------------------------------------------------
/example/android/app/src/main/jni/MainComponentsRegistry.cpp:
--------------------------------------------------------------------------------
1 | #include "MainComponentsRegistry.h"
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 |
9 | namespace facebook {
10 | namespace react {
11 |
12 | MainComponentsRegistry::MainComponentsRegistry(ComponentFactory *delegate) {}
13 |
14 | std::shared_ptr
15 | MainComponentsRegistry::sharedProviderRegistry() {
16 | auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry();
17 |
18 | // Autolinked providers registered by RN CLI
19 | rncli_registerProviders(providerRegistry);
20 |
21 | // Custom Fabric Components go here. You can register custom
22 | // components coming from your App or from 3rd party libraries here.
23 | //
24 | // providerRegistry->add(concreteComponentDescriptorProvider<
25 | // AocViewerComponentDescriptor>());
26 | return providerRegistry;
27 | }
28 |
29 | jni::local_ref
30 | MainComponentsRegistry::initHybrid(
31 | jni::alias_ref,
32 | ComponentFactory *delegate) {
33 | auto instance = makeCxxInstance(delegate);
34 |
35 | auto buildRegistryFunction =
36 | [](EventDispatcher::Weak const &eventDispatcher,
37 | ContextContainer::Shared const &contextContainer)
38 | -> ComponentDescriptorRegistry::Shared {
39 | auto registry = MainComponentsRegistry::sharedProviderRegistry()
40 | ->createComponentDescriptorRegistry(
41 | {eventDispatcher, contextContainer});
42 |
43 | auto mutableRegistry =
44 | std::const_pointer_cast(registry);
45 |
46 | mutableRegistry->setFallbackComponentDescriptor(
47 | std::make_shared(
48 | ComponentDescriptorParameters{
49 | eventDispatcher, contextContainer, nullptr}));
50 |
51 | return registry;
52 | };
53 |
54 | delegate->buildRegistryFunction = buildRegistryFunction;
55 | return instance;
56 | }
57 |
58 | void MainComponentsRegistry::registerNatives() {
59 | registerHybrid({
60 | makeNativeMethod("initHybrid", MainComponentsRegistry::initHybrid),
61 | });
62 | }
63 |
64 | } // namespace react
65 | } // namespace facebook
66 |
--------------------------------------------------------------------------------
/ios/RTMPModule/RTMPModule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RTMPManager.swift
3 | // rtmpPackageExample
4 | //
5 | // Created by Ezran Bayantemur on 15.01.2022.
6 | //
7 |
8 | import AudioToolbox
9 | import AVFoundation
10 | import HaishinKit
11 |
12 | // TODO: Try catch blokları eklenecek
13 |
14 | @objc(RTMPPublisher)
15 | class RTMPModule: NSObject {
16 | private var cameraPosition: AVCaptureDevice.Position = .back
17 |
18 | @objc
19 | func startStream(_ resolve: (RCTPromiseResolveBlock), reject: (RCTPromiseRejectBlock)){
20 | RTMPCreator.startPublish()
21 | }
22 |
23 | @objc
24 | func stopStream(_ resolve: (RCTPromiseResolveBlock), reject: (RCTPromiseRejectBlock)){
25 | RTMPCreator.stopPublish()
26 | }
27 |
28 | @objc
29 | func mute(_ resolve: (RCTPromiseResolveBlock), reject: (RCTPromiseRejectBlock)){
30 | RTMPCreator.stream.audioSettings[.muted] = true
31 | }
32 |
33 | @objc
34 | func unmute(_ resolve: (RCTPromiseResolveBlock), reject: (RCTPromiseRejectBlock)){
35 | RTMPCreator.stream.audioSettings[.muted] = false
36 | }
37 |
38 | @objc
39 | func switchCamera(_ resolve: (RCTPromiseResolveBlock), reject: (RCTPromiseRejectBlock)){
40 | cameraPosition = cameraPosition == .back ? .front : .back
41 | RTMPCreator.stream.attachCamera(DeviceUtil.device(withPosition: cameraPosition))
42 | }
43 |
44 | @objc
45 | func getPublishURL(_ resolve: (RCTPromiseResolveBlock), reject: (RCTPromiseRejectBlock)){
46 | resolve(RTMPCreator.getPublishURL())
47 | }
48 |
49 | @objc
50 | func isMuted(_ resolve: (RCTPromiseResolveBlock), reject: (RCTPromiseRejectBlock)){
51 | resolve(RTMPCreator.stream.audioSettings[.muted])
52 | }
53 |
54 | @objc
55 | func isStreaming(_ resolve: (RCTPromiseResolveBlock), reject: (RCTPromiseRejectBlock)){
56 | resolve(RTMPCreator.isStreaming)
57 | }
58 |
59 | @objc
60 | func isAudioPrepared(_ resolve: (RCTPromiseResolveBlock), reject: (RCTPromiseRejectBlock)){
61 | resolve(RTMPCreator.stream.receiveAudio)
62 | }
63 |
64 | @objc
65 | func isVideoPrepared(_ resolve: (RCTPromiseResolveBlock), reject: (RCTPromiseRejectBlock)){
66 | resolve(RTMPCreator.stream.receiveVideo)
67 | }
68 |
69 | @objc
70 | func toggleFlash(_ resolve: (RCTPromiseResolveBlock), reject: (RCTPromiseRejectBlock)){
71 | resolve(RTMPCreator.stream.torch.toggle())
72 | }
73 |
74 | @objc
75 | func setAudioInput(_ audioInput: (NSInteger), resolve: (RCTPromiseResolveBlock), reject: (RCTPromiseRejectBlock)){
76 | resolve(RTMPCreator.setAudioInput(audioInput: audioInput))
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/ios/RTMPModule/RTMPModule.m:
--------------------------------------------------------------------------------
1 | //
2 | // RTMPManager.m
3 | // rtmpPackageExample
4 | //
5 | // Created by Ezran Bayantemur on 15.01.2022.
6 | //
7 |
8 | #import
9 |
10 | @interface RCT_EXTERN_MODULE(RTMPPublisher, NSObject)
11 |
12 | + (BOOL)requiresMainQueueSetup
13 | {
14 | return NO;
15 | }
16 |
17 | RCT_EXTERN_METHOD(
18 | startStream: (RCTPromiseResolveBlock)resolve
19 | reject: (RCTPromiseRejectBlock)reject
20 | )
21 |
22 | RCT_EXTERN_METHOD(
23 | stopStream: (RCTPromiseResolveBlock)resolve
24 | reject: (RCTPromiseRejectBlock)reject
25 | )
26 |
27 | RCT_EXTERN_METHOD(
28 | mute: (RCTPromiseResolveBlock)resolve
29 | reject: (RCTPromiseRejectBlock)reject
30 | )
31 |
32 | RCT_EXTERN_METHOD(
33 | unmute: (RCTPromiseResolveBlock)resolve
34 | reject: (RCTPromiseRejectBlock)reject
35 | )
36 |
37 | RCT_EXTERN_METHOD(
38 | switchCamera: (RCTPromiseResolveBlock)resolve
39 | reject: (RCTPromiseRejectBlock)reject
40 | )
41 |
42 | RCT_EXTERN_METHOD(
43 | getPublishURL: (RCTPromiseResolveBlock)resolve
44 | reject: (RCTPromiseRejectBlock)reject
45 | )
46 |
47 | RCT_EXTERN_METHOD(
48 | isMuted: (RCTPromiseResolveBlock)resolve
49 | reject: (RCTPromiseRejectBlock)reject
50 | )
51 |
52 | RCT_EXTERN_METHOD(
53 | isStreaming: (RCTPromiseResolveBlock)resolve
54 | reject: (RCTPromiseRejectBlock)reject
55 | )
56 |
57 | RCT_EXTERN_METHOD(
58 | isAudioPrepared: (RCTPromiseResolveBlock)resolve
59 | reject: (RCTPromiseRejectBlock)reject
60 | )
61 |
62 | RCT_EXTERN_METHOD(
63 | isVideoPrepared: (RCTPromiseResolveBlock)resolve
64 | reject: (RCTPromiseRejectBlock)reject
65 | )
66 |
67 | RCT_EXTERN_METHOD(
68 | isCameraOnPreview: (RCTPromiseResolveBlock)resolve
69 | reject: (RCTPromiseRejectBlock)reject
70 | )
71 |
72 | RCT_EXTERN_METHOD(
73 | toggleFlash: (RCTPromiseResolveBlock)resolve
74 | reject: (RCTPromiseRejectBlock)reject
75 | )
76 |
77 |
78 | RCT_EXTERN_METHOD(
79 | setAudioInput: (NSInteger)audioInput
80 | resolve: (RCTPromiseResolveBlock)resolve
81 | reject: (RCTPromiseRejectBlock)reject
82 | )
83 |
84 | @end
85 |
--------------------------------------------------------------------------------
/android/src/main/java/com/reactnativertmppublisher/modules/BluetoothDeviceConnector.java:
--------------------------------------------------------------------------------
1 | package com.reactnativertmppublisher.modules;
2 |
3 | import android.bluetooth.BluetoothAdapter;
4 | import android.bluetooth.BluetoothProfile;
5 | import android.content.BroadcastReceiver;
6 | import android.content.Context;
7 | import android.content.Intent;
8 | import android.content.IntentFilter;
9 | import android.util.Log;
10 |
11 | import com.reactnativertmppublisher.enums.BluetoothDeviceStatuses;
12 | import com.reactnativertmppublisher.interfaces.ConnectionListener;
13 |
14 | import java.util.ArrayList;
15 | import java.util.List;
16 |
17 | public class BluetoothDeviceConnector extends BroadcastReceiver implements BluetoothProfile.ServiceListener{
18 | private final List listeners = new ArrayList<>();
19 |
20 | public void addListener(ConnectionListener listener) {
21 | listeners.add(listener);
22 | }
23 |
24 | public BluetoothDeviceConnector(Context context) {
25 | BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
26 | mBluetoothAdapter.getProfileProxy(context, this, BluetoothProfile.HEADSET);
27 | context.registerReceiver(this, new IntentFilter(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED));
28 | }
29 |
30 | @Override
31 | public void onServiceConnected(int i, BluetoothProfile bluetoothProfile) {
32 | if(bluetoothProfile.getConnectedDevices().size() > 0) {
33 | for (ConnectionListener l : listeners) {
34 | l.onChange("onBluetoothDeviceStatusChanged", BluetoothDeviceStatuses.CONNECTED.toString());
35 | }
36 | }
37 | }
38 |
39 | @Override
40 | public void onServiceDisconnected(int i) {
41 | for (ConnectionListener l : listeners) {
42 | l.onChange("onBluetoothDeviceStatusChanged", BluetoothDeviceStatuses.DISCONNECTED.toString());
43 | }
44 | }
45 |
46 | @Override
47 | public void onReceive(Context context, Intent intent) {
48 | int status = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, -1);
49 |
50 | switch (status){
51 | case BluetoothAdapter.STATE_CONNECTING: {
52 | for (ConnectionListener l : listeners) {
53 | l.onChange("onBluetoothDeviceStatusChanged", BluetoothDeviceStatuses.CONNECTING.toString());
54 | };
55 | break;
56 | }
57 |
58 | case BluetoothAdapter.STATE_CONNECTED: {
59 | for (ConnectionListener l : listeners) {
60 | l.onChange("onBluetoothDeviceStatusChanged", BluetoothDeviceStatuses.CONNECTED.toString());
61 | };
62 | break;
63 | }
64 |
65 | case BluetoothAdapter.STATE_DISCONNECTED: {
66 | for (ConnectionListener l : listeners) {
67 | l.onChange("onBluetoothDeviceStatusChanged", BluetoothDeviceStatuses.DISCONNECTED.toString());
68 | };
69 | break;
70 | }
71 | };
72 |
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/coverage/lcov-report/block-navigation.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var jumpToCode = (function init() {
3 | // Classes of code we would like to highlight in the file view
4 | var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
5 |
6 | // Elements to highlight in the file listing view
7 | var fileListingElements = ['td.pct.low'];
8 |
9 | // We don't want to select elements that are direct descendants of another match
10 | var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
11 |
12 | // Selecter that finds elements on the page to which we can jump
13 | var selector =
14 | fileListingElements.join(', ') +
15 | ', ' +
16 | notSelector +
17 | missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
18 |
19 | // The NodeList of matching elements
20 | var missingCoverageElements = document.querySelectorAll(selector);
21 |
22 | var currentIndex;
23 |
24 | function toggleClass(index) {
25 | missingCoverageElements
26 | .item(currentIndex)
27 | .classList.remove('highlighted');
28 | missingCoverageElements.item(index).classList.add('highlighted');
29 | }
30 |
31 | function makeCurrent(index) {
32 | toggleClass(index);
33 | currentIndex = index;
34 | missingCoverageElements.item(index).scrollIntoView({
35 | behavior: 'smooth',
36 | block: 'center',
37 | inline: 'center'
38 | });
39 | }
40 |
41 | function goToPrevious() {
42 | var nextIndex = 0;
43 | if (typeof currentIndex !== 'number' || currentIndex === 0) {
44 | nextIndex = missingCoverageElements.length - 1;
45 | } else if (missingCoverageElements.length > 1) {
46 | nextIndex = currentIndex - 1;
47 | }
48 |
49 | makeCurrent(nextIndex);
50 | }
51 |
52 | function goToNext() {
53 | var nextIndex = 0;
54 |
55 | if (
56 | typeof currentIndex === 'number' &&
57 | currentIndex < missingCoverageElements.length - 1
58 | ) {
59 | nextIndex = currentIndex + 1;
60 | }
61 |
62 | makeCurrent(nextIndex);
63 | }
64 |
65 | return function jump(event) {
66 | if (
67 | document.getElementById('fileSearch') === document.activeElement &&
68 | document.activeElement != null
69 | ) {
70 | // if we're currently focused on the search input, we don't want to navigate
71 | return;
72 | }
73 |
74 | switch (event.which) {
75 | case 78: // n
76 | case 74: // j
77 | goToNext();
78 | break;
79 | case 66: // b
80 | case 75: // k
81 | case 80: // p
82 | goToPrevious();
83 | break;
84 | }
85 | };
86 | })();
87 | window.addEventListener('keydown', jumpToCode);
88 |
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/example/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
--------------------------------------------------------------------------------
/android/src/main/java/com/reactnativertmppublisher/RTMPManager.java:
--------------------------------------------------------------------------------
1 | package com.reactnativertmppublisher;
2 |
3 | import android.view.SurfaceView;
4 | import android.view.View;
5 |
6 | import androidx.annotation.NonNull;
7 | import androidx.annotation.Nullable;
8 |
9 | import com.facebook.react.common.MapBuilder;
10 | import com.facebook.react.uimanager.SimpleViewManager;
11 | import com.facebook.react.uimanager.ThemedReactContext;
12 | import com.facebook.react.uimanager.annotations.ReactProp;
13 | import com.facebook.react.uimanager.events.RCTEventEmitter;
14 | import com.reactnativertmppublisher.modules.Publisher;
15 | import com.reactnativertmppublisher.modules.SurfaceHolderHelper;
16 |
17 | import java.util.Map;
18 |
19 | public class RTMPManager extends SimpleViewManager {
20 | //TODO: "Do not place Android context classes in static fields (static reference to Publisher which has field _surfaceView pointing to SurfaceView); this is a memory leak"
21 | public static Publisher publisher;
22 | public final String REACT_CLASS_NAME = "RTMPPublisher";
23 | SurfaceView surfaceView;
24 | private ThemedReactContext _reactContext;
25 |
26 | View.OnLayoutChangeListener onLayoutChangeListener = new View.OnLayoutChangeListener() {
27 | @Override
28 | public void onLayoutChange(@NonNull View view, int i, int i1, int i2, int i3, int i4, int i5, int i6, int i7) {
29 |
30 | }
31 | };
32 |
33 | @NonNull
34 | @Override
35 | public String getName() {
36 | return REACT_CLASS_NAME;
37 | }
38 |
39 | @NonNull
40 | @Override
41 | protected SurfaceView createViewInstance(@NonNull ThemedReactContext reactContext) {
42 | _reactContext = reactContext;
43 | surfaceView = new SurfaceView(_reactContext);
44 | publisher = new Publisher(_reactContext, surfaceView);
45 | surfaceView.addOnLayoutChangeListener(onLayoutChangeListener);
46 |
47 | SurfaceHolderHelper surfaceHolderHelper = new SurfaceHolderHelper(_reactContext, publisher.getRtmpCamera(), surfaceView.getId());
48 | surfaceView.getHolder().addCallback(surfaceHolderHelper);
49 |
50 | return surfaceView;
51 | }
52 |
53 | @ReactProp(name = "streamURL")
54 | public void setStreamURL(SurfaceView surfaceView, @Nullable String url) {
55 | publisher.setStreamUrl(url);
56 | }
57 |
58 | @ReactProp(name = "streamName")
59 | public void setStreamName(SurfaceView surfaceView, @Nullable String name) {
60 | publisher.setStreamName(name);
61 | }
62 |
63 | @Nullable
64 | @Override
65 | public Map getExportedCustomDirectEventTypeConstants() {
66 | return MapBuilder.builder()
67 | .put("onDisconnect", MapBuilder.of("registrationName", "onDisconnect"))
68 | .put("onConnectionFailed", MapBuilder.of("registrationName", "onConnectionFailed"))
69 | .put("onConnectionStarted", MapBuilder.of("registrationName", "onConnectionStarted"))
70 | .put("onConnectionSuccess", MapBuilder.of("registrationName", "onConnectionSuccess"))
71 | .put("onNewBitrateReceived", MapBuilder.of("registrationName", "onNewBitrateReceived"))
72 | .put("onStreamStateChanged", MapBuilder.of("registrationName", "onStreamStateChanged"))
73 | .put("onBluetoothDeviceStatusChanged", MapBuilder.of("registrationName", "onBluetoothDeviceStatusChanged"))
74 | .build();
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/com/example/reactnativertmp/MainApplication.java:
--------------------------------------------------------------------------------
1 | package com.example.reactnativertmp;
2 |
3 | import android.app.Application;
4 | import android.content.Context;
5 | import com.facebook.react.PackageList;
6 | import com.facebook.react.ReactApplication;
7 | import com.facebook.react.ReactNativeHost;
8 | import com.facebook.react.ReactPackage;
9 | import com.facebook.react.ReactInstanceManager;
10 | import com.facebook.react.config.ReactFeatureFlags;
11 | import com.facebook.soloader.SoLoader;
12 | import com.example.reactnativertmp.newarchitecture.MainApplicationReactNativeHost;
13 | import java.lang.reflect.InvocationTargetException;
14 | import java.util.List;
15 | import com.reactnativertmppublisher.RTMPPackage;
16 |
17 | public class MainApplication extends Application implements ReactApplication {
18 |
19 | private final ReactNativeHost mReactNativeHost =
20 | new ReactNativeHost(this) {
21 | @Override
22 | public boolean getUseDeveloperSupport() {
23 | return BuildConfig.DEBUG;
24 | }
25 |
26 | @Override
27 | protected List getPackages() {
28 | @SuppressWarnings("UnnecessaryLocalVariable")
29 | List packages = new PackageList(this).getPackages();
30 | // Packages that cannot be autolinked yet can be added manually here, for RtmpExample:
31 | // packages.add(new MyReactNativePackage());
32 | packages.add(new RTMPPackage());
33 | return packages;
34 | }
35 |
36 | @Override
37 | protected String getJSMainModuleName() {
38 | return "index";
39 | }
40 | };
41 |
42 | private final ReactNativeHost mNewArchitectureNativeHost =
43 | new MainApplicationReactNativeHost(this);
44 |
45 | @Override
46 | public ReactNativeHost getReactNativeHost() {
47 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
48 | return mNewArchitectureNativeHost;
49 | } else {
50 | return mReactNativeHost;
51 | }
52 | }
53 |
54 | @Override
55 | public void onCreate() {
56 | super.onCreate();
57 | // If you opted-in for the New Architecture, we enable the TurboModule system
58 | ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
59 | SoLoader.init(this, /* native exopackage */ false);
60 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); // Remove this line if you don't want Flipper enabled
61 | }
62 |
63 | /**
64 | * Loads Flipper in React Native templates.
65 | *
66 | * @param context
67 | */
68 | private static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
69 | if (BuildConfig.DEBUG) {
70 | try {
71 | /*
72 | We use reflection here to pick up the class that initializes Flipper,
73 | since Flipper library is not available in release mode
74 | */
75 | Class> aClass = Class.forName("com.example.reactnativertmp.ReactNativeFlipper");
76 | aClass
77 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
78 | .invoke(null, context, reactInstanceManager);
79 | } catch (ClassNotFoundException e) {
80 | e.printStackTrace();
81 | } catch (NoSuchMethodException e) {
82 | e.printStackTrace();
83 | } catch (IllegalAccessException e) {
84 | e.printStackTrace();
85 | } catch (InvocationTargetException e) {
86 | e.printStackTrace();
87 | }
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/example/android/app/src/debug/java/com/example/reactnativertmp/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its 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.example.reactnativertmp;
8 |
9 | import android.content.Context;
10 | import com.facebook.flipper.android.AndroidFlipperClient;
11 | import com.facebook.flipper.android.utils.FlipperUtils;
12 | import com.facebook.flipper.core.FlipperClient;
13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping;
17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
20 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
22 | import com.facebook.react.ReactInstanceEventListener;
23 | import com.facebook.react.ReactInstanceManager;
24 | import com.facebook.react.bridge.ReactContext;
25 | import com.facebook.react.modules.network.NetworkingModule;
26 | import okhttp3.OkHttpClient;
27 |
28 | public class ReactNativeFlipper {
29 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
30 | if (FlipperUtils.shouldEnableFlipper(context)) {
31 | final FlipperClient client = AndroidFlipperClient.getInstance(context);
32 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
33 | client.addPlugin(new ReactFlipperPlugin());
34 | client.addPlugin(new DatabasesFlipperPlugin(context));
35 | client.addPlugin(new SharedPreferencesFlipperPlugin(context));
36 | client.addPlugin(CrashReporterPlugin.getInstance());
37 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
38 | NetworkingModule.setCustomClientBuilder(
39 | new NetworkingModule.CustomClientBuilder() {
40 | @Override
41 | public void apply(OkHttpClient.Builder builder) {
42 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
43 | }
44 | });
45 | client.addPlugin(networkFlipperPlugin);
46 | client.start();
47 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
48 | // Hence we run if after all native modules have been initialized
49 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
50 | if (reactContext == null) {
51 | reactInstanceManager.addReactInstanceEventListener(
52 | new ReactInstanceEventListener() {
53 | @Override
54 | public void onReactContextInitialized(ReactContext reactContext) {
55 | reactInstanceManager.removeReactInstanceEventListener(this);
56 | reactContext.runOnNativeModulesQueueThread(
57 | new Runnable() {
58 | @Override
59 | public void run() {
60 | client.addPlugin(new FrescoFlipperPlugin());
61 | }
62 | });
63 | }
64 | });
65 | } else {
66 | client.addPlugin(new FrescoFlipperPlugin());
67 | }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 | - dev
8 |
9 | jobs:
10 | build_dependency:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - name: Caching node_modules
17 | id: cache-npm
18 | uses: actions/cache@v3
19 | env:
20 | cache-name: cache-node-modules
21 | with:
22 | path: ./node_modules
23 | key: rtmp-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}
24 |
25 | - name: Caching Pods
26 | id: cache-pods
27 | uses: actions/cache@v3
28 | env:
29 | cache-name: cache-cocoapods
30 | with:
31 | path: ./example/ios/Pods
32 | key: rtmp-build-${{ env.cache-name }}-${{ hashFiles('./example/ios/Podfile.lock') }}
33 |
34 | - name: Install dependencies
35 | run: |
36 | yarn
37 | - name: Lint files
38 | run: |
39 | yarn lint
40 | - name: Typescript checking
41 | run: |
42 | yarn typescript
43 | - name: Unit tests
44 | run: |
45 | yarn test --coverage --updateSnapshot --verbose
46 | - name: Building the package
47 | run: |
48 | yarn prepare
49 | build_android:
50 | needs: build_dependency
51 | runs-on: ubuntu-latest
52 |
53 | steps:
54 | - uses: actions/checkout@v3
55 |
56 | - name: Caching node_modules
57 | id: cache-npm
58 | uses: actions/cache@v3
59 | env:
60 | cache-name: cache-node-modules
61 | with:
62 | path: ./node_modules
63 | key: rtmp-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}
64 |
65 | - name: Install dependencies
66 | run: |
67 | yarn
68 | - name: Caching Gradle
69 | id: cache-gradle
70 | uses: actions/cache@v3
71 | env:
72 | cache-name: cache-gradle-files
73 | with:
74 | path: ./example/android/.gradle
75 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./example/android/gradle/wrapper/gradle-wrapper.properties') }}
76 | restore-keys: |
77 | ${{ runner.os }}-build-${{ env.cache-name }}-
78 | - name: Install dependencies
79 | run: |
80 | yarn
81 | - name: Building Android
82 | run: |
83 | cd example/android && ./gradlew build
84 | build_ios:
85 | needs: build_dependency
86 | runs-on: macos-latest
87 |
88 | steps:
89 | - uses: actions/checkout@v3
90 |
91 | - name: Caching node_modules
92 | id: cache-npm
93 | uses: actions/cache@v3
94 | env:
95 | cache-name: cache-node-modules
96 | with:
97 | path: ./node_modules
98 | key: rtmp-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}
99 |
100 | - name: Caching Pods
101 | id: cache-pods
102 | uses: actions/cache@v3
103 | env:
104 | cache-name: cache-cocoapods
105 | with:
106 | path: ./example/ios/Pods
107 | key: rtmp-build-${{ env.cache-name }}-${{ hashFiles('./example/ios/Podfile.lock') }}
108 |
109 | - name: Install dependencies
110 | run: |
111 | yarn
112 | - name: Building iOS
113 | run: |
114 | cd example/ios && xcodebuild -quiet -workspace RtmpExample.xcworkspace -scheme RtmpExample -destination generic/platform=iOS CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO build
115 |
--------------------------------------------------------------------------------
/src/test/RTMPPublisher.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { fireEvent, render } from '@testing-library/react-native';
3 | import RTMPPublisher, { RTMPPublisherProps } from '../RTMPPublisher';
4 |
5 | const onDisconnectMock = jest.fn();
6 | const onConnectionFailedMock = jest.fn();
7 | const onConnectionStartedMock = jest.fn();
8 | const onConnectionSuccessMock = jest.fn();
9 | const onNewBitrateReceivedMock = jest.fn();
10 | const onStreamStateChangedMock = jest.fn();
11 | const onBluetoothDeviceStatusChangedMock = jest.fn();
12 |
13 | const mockProps: RTMPPublisherProps = {
14 | testID: 'publisher',
15 | streamName: 'test-stream',
16 | streamURL: 'rtmp-test-stream',
17 | onDisconnect: onDisconnectMock,
18 | onConnectionFailed: onConnectionFailedMock,
19 | onConnectionStarted: onConnectionStartedMock,
20 | onConnectionSuccess: onConnectionSuccessMock,
21 | onNewBitrateReceived: onNewBitrateReceivedMock,
22 | onStreamStateChanged: onStreamStateChangedMock,
23 | onBluetoothDeviceStatusChanged: onBluetoothDeviceStatusChangedMock,
24 | };
25 |
26 | describe('RTMPPublisher tests', () => {
27 | let wrapper: ReturnType;
28 |
29 | beforeEach(() => {
30 | wrapper = render();
31 | });
32 |
33 | test('should match with snapshot', () => {
34 | expect(wrapper).toMatchSnapshot();
35 | });
36 |
37 | test('should trigger onDisconnect', () => {
38 | const data = 'test-disconnect';
39 | fireEvent(wrapper.getByTestId('publisher'), 'onDisconnect', {
40 | nativeEvent: { data },
41 | });
42 |
43 | expect(onDisconnectMock).toHaveBeenCalledWith(data);
44 | });
45 |
46 | test('should trigger onConnectionFailed', () => {
47 | const data = 'test-connection-failed';
48 | fireEvent(wrapper.getByTestId('publisher'), 'onConnectionFailed', {
49 | nativeEvent: { data },
50 | });
51 |
52 | expect(onConnectionFailedMock).toHaveBeenCalledWith(data);
53 | });
54 |
55 | test('should trigger onConnectionStarted', () => {
56 | const data = 'test-connection-started';
57 | fireEvent(wrapper.getByTestId('publisher'), 'onConnectionStarted', {
58 | nativeEvent: { data },
59 | });
60 |
61 | expect(onConnectionStartedMock).toHaveBeenCalledWith(data);
62 | });
63 |
64 | test('should trigger onConnectionSuccess', () => {
65 | const data = 'test-connection-success';
66 | fireEvent(wrapper.getByTestId('publisher'), 'onConnectionSuccess', {
67 | nativeEvent: { data },
68 | });
69 |
70 | expect(onConnectionSuccessMock).toHaveBeenCalledWith(data);
71 | });
72 |
73 | test('should trigger onNewBitrateReceived', () => {
74 | const data = 'test-new-bitrate-received';
75 | fireEvent(wrapper.getByTestId('publisher'), 'onNewBitrateReceived', {
76 | nativeEvent: { data },
77 | });
78 |
79 | expect(onNewBitrateReceivedMock).toHaveBeenCalledWith(data);
80 | });
81 |
82 | test('should trigger onStreamStateChanged', () => {
83 | const data = 'test-stream-state-changed';
84 | fireEvent(wrapper.getByTestId('publisher'), 'onStreamStateChanged', {
85 | nativeEvent: { data },
86 | });
87 |
88 | expect(onStreamStateChangedMock).toHaveBeenCalledWith(data);
89 | });
90 |
91 | test('should trigger onBluetoothDeviceStatusChanged', () => {
92 | const data = 'test-bluetooth-device-status-changed';
93 | fireEvent(
94 | wrapper.getByTestId('publisher'),
95 | 'onBluetoothDeviceStatusChanged',
96 | { nativeEvent: { data } }
97 | );
98 |
99 | expect(onBluetoothDeviceStatusChangedMock).toHaveBeenCalledWith(data);
100 | });
101 | });
102 |
--------------------------------------------------------------------------------
/ios/RTMPCreator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RTMPCreator.swift
3 | // rtmpPackageExample
4 | //
5 | // Created by Ezran Bayantemur on 15.01.2022.
6 | //
7 | import HaishinKit
8 | import AVFoundation
9 |
10 | class RTMPCreator {
11 | public static let connection: RTMPConnection = RTMPConnection()
12 | public static let stream: RTMPStream = RTMPStream(connection: connection)
13 | private static let session = AVAudioSession.sharedInstance()
14 | private static var _streamUrl: String = ""
15 | private static var _streamName: String = ""
16 | public static var isStreaming: Bool = false
17 |
18 | public static func setStreamUrl(url: String){
19 | _streamUrl = url
20 | }
21 |
22 | public static func setStreamName(name: String){
23 | _streamName = name
24 | }
25 |
26 |
27 | public static func getPublishURL() -> String {
28 | // TODO: Object formatına dönüştürülebilir
29 | /**
30 | {
31 | streamName: _streamName
32 | streamUrl: _streamUrl
33 | }
34 | */
35 | return "\(_streamUrl)/\(_streamName)"
36 | }
37 |
38 | public static func startPublish(){
39 | connection.requireNetworkFramework = true
40 | connection.connect(_streamUrl)
41 | stream.publish(_streamName)
42 | isStreaming = true
43 | }
44 |
45 | public static func stopPublish(){
46 | stream.close()
47 | connection.close()
48 | isStreaming = false
49 | }
50 |
51 | public static func setAudioInput(audioInput: Int){
52 | switch audioInput {
53 | case 0:
54 | switchToBluetooth()
55 | break;
56 |
57 | case 1:
58 | switchToSpeaker()
59 | break;
60 |
61 | case 2:
62 | switchToHeadset()
63 | break;
64 |
65 | default:
66 | return;
67 | }
68 | }
69 |
70 | private static func switchToSpeaker(){
71 | let inputs: [AVAudioSessionPortDescription] = session.availableInputs!
72 |
73 | if let selectedDesc = inputs.first(where: { (desc) -> Bool in
74 | return desc.portType == AVAudioSession.Port.builtInMic
75 | }){
76 | do{
77 |
78 | let selectedDataSource = selectedDesc.dataSources?.first(where: { (source) -> Bool in
79 | return source.orientation == AVAudioSession.Orientation.front
80 | })
81 |
82 | try session.setPreferredInput(selectedDesc)
83 | try session.setInputDataSource(selectedDataSource)
84 | } catch let error{
85 | print(error)
86 | }
87 | }
88 | }
89 |
90 | private static func switchToHeadset(){
91 | let inputs: [AVAudioSessionPortDescription] = session.availableInputs!
92 |
93 | if let selectedDesc = inputs.first(where: { (desc) -> Bool in
94 | return desc.portType == AVAudioSession.Port.headsetMic
95 | }){
96 | do{
97 | try session.setPreferredInput(selectedDesc)
98 | } catch let error{
99 | print(error)
100 | }
101 | }
102 | }
103 |
104 | private static func switchToBluetooth(){
105 | let inputs: [AVAudioSessionPortDescription] = session.availableInputs!
106 |
107 | if let selectedDesc = inputs.first(where: { (desc) -> Bool in
108 | return desc.portType == AVAudioSession.Port.bluetoothHFP
109 | }){
110 | do{
111 | try session.setPreferredInput(selectedDesc)
112 | } catch let error{
113 | print(error)
114 | }
115 | }
116 | }
117 |
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/ios/RTMPManager/RTMPView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RTMPView.swift
3 | // rtmpPackageExample
4 | //
5 | // Created by Ezran Bayantemur on 15.01.2022.
6 | //
7 |
8 | import UIKit
9 | import HaishinKit
10 | import AVFoundation
11 |
12 | class RTMPView: UIView {
13 | private var hkView: MTHKView!
14 | @objc var onDisconnect: RCTDirectEventBlock?
15 | @objc var onConnectionFailed: RCTDirectEventBlock?
16 | @objc var onConnectionStarted: RCTDirectEventBlock?
17 | @objc var onConnectionSuccess: RCTDirectEventBlock?
18 | @objc var onNewBitrateReceived: RCTDirectEventBlock?
19 | @objc var onStreamStateChanged: RCTDirectEventBlock?
20 |
21 | @objc var streamURL: NSString = "" {
22 | didSet {
23 | RTMPCreator.setStreamUrl(url: streamURL as String)
24 | }
25 | }
26 |
27 | @objc var streamName: NSString = "" {
28 | didSet {
29 | RTMPCreator.setStreamName(name: streamName as String)
30 | }
31 | }
32 |
33 | override init(frame: CGRect) {
34 | super.init(frame: frame)
35 | UIApplication.shared.isIdleTimerDisabled = true
36 |
37 | hkView = MTHKView(frame: UIScreen.main.bounds)
38 | hkView.videoGravity = .resizeAspectFill
39 |
40 | RTMPCreator.stream.captureSettings = [
41 | .fps: 30,
42 | .sessionPreset: AVCaptureSession.Preset.hd1920x1080,
43 | .continuousAutofocus: true,
44 | .continuousExposure: true
45 | ]
46 |
47 | RTMPCreator.stream.videoSettings = [
48 | .width: 720,
49 | .height: 1280,
50 | .bitrate: 3000 * 1024,
51 | .scalingMode: ScalingMode.cropSourceToCleanAperture
52 |
53 | ]
54 |
55 | RTMPCreator.stream.attachAudio(AVCaptureDevice.default(for: .audio))
56 | RTMPCreator.stream.attachCamera(DeviceUtil.device(withPosition: AVCaptureDevice.Position.back))
57 |
58 | RTMPCreator.connection.addEventListener(.rtmpStatus, selector: #selector(statusHandler), observer: self)
59 |
60 | hkView.attachStream(RTMPCreator.stream)
61 |
62 | self.addSubview(hkView)
63 |
64 | }
65 |
66 | required init?(coder aDecoder: NSCoder) {
67 | fatalError("init(coder:) has not been implemented")
68 | }
69 |
70 | override func removeFromSuperview() {
71 | print("ON REMOVE")
72 | }
73 |
74 | @objc
75 | private func statusHandler(_ notification: Notification){
76 | let e = Event.from(notification)
77 | guard let data: ASObject = e.data as? ASObject, let code: String = data["code"] as? String else {
78 | return
79 | }
80 |
81 | switch code {
82 | case RTMPConnection.Code.connectSuccess.rawValue:
83 | if onConnectionSuccess != nil {
84 | onConnectionSuccess!(nil)
85 | }
86 | changeStreamState(status: "CONNECTING")
87 | break
88 |
89 | case RTMPConnection.Code.connectFailed.rawValue:
90 | if onConnectionFailed != nil {
91 | onConnectionFailed!(nil)
92 | }
93 | changeStreamState(status: "FAILED")
94 | break
95 |
96 | case RTMPConnection.Code.connectClosed.rawValue:
97 | if onDisconnect != nil {
98 | onDisconnect!(nil)
99 | }
100 | break
101 |
102 | case RTMPStream.Code.publishStart.rawValue:
103 | if onConnectionStarted != nil {
104 | onConnectionStarted!(nil)
105 | }
106 | changeStreamState(status: "CONNECTED")
107 | break
108 |
109 | default:
110 | break
111 | }
112 | }
113 |
114 | public func changeStreamState(status: String){
115 | if onStreamStateChanged != nil {
116 | onStreamStateChanged!(["data": status])
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/coverage/lcov-report/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Code coverage report for All files
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
All files
23 |
24 |
25 |
26 | Unknown%
27 | Statements
28 | 0/0
29 |
30 |
31 |
32 |
33 | Unknown%
34 | Branches
35 | 0/0
36 |
37 |
38 |
39 |
40 | Unknown%
41 | Functions
42 | 0/0
43 |
44 |
45 |
46 |
47 | Unknown%
48 | Lines
49 | 0/0
50 |
51 |
52 |
53 |
54 |
55 | Press n or j to go to the next uncovered block, b, p or k for the previous block.
56 |
57 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | | File |
70 | |
71 | Statements |
72 | |
73 | Branches |
74 | |
75 | Functions |
76 | |
77 | Lines |
78 | |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
91 |
92 |
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/example/ios/RtmpExample.xcodeproj/xcshareddata/xcschemes/RtmpExample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
51 |
52 |
53 |
54 |
55 |
56 |
66 |
68 |
74 |
75 |
76 |
77 |
83 |
85 |
91 |
92 |
93 |
94 |
96 |
97 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-rtmp-publisher",
3 | "version": "0.4.7",
4 | "description": "RTM Publisher for React Native",
5 | "main": "lib/commonjs/index",
6 | "module": "lib/module/index",
7 | "types": "lib/typescript/index.d.ts",
8 | "react-native": "src/index",
9 | "source": "src/index",
10 | "files": [
11 | "src",
12 | "lib",
13 | "android",
14 | "ios",
15 | "cpp",
16 | "react-native-rtmp-publisher.podspec",
17 | "!lib/typescript/example",
18 | "!android/build",
19 | "!ios/build",
20 | "!**/__tests__",
21 | "!**/__fixtures__",
22 | "!**/__mocks__"
23 | ],
24 | "scripts": {
25 | "test": "jest",
26 | "typescript": "tsc --noEmit",
27 | "lint": "eslint \"**/*.{js,ts,tsx}\"",
28 | "prepare": "bob build",
29 | "release": "dotenv release-it --",
30 | "example": "yarn --cwd example",
31 | "pods": "cd example && pod-install --quiet",
32 | "bootstrap": "yarn example && yarn && yarn pods"
33 | },
34 | "keywords": [
35 | "react-native",
36 | "ios",
37 | "android"
38 | ],
39 | "repository": {
40 | "type": "git",
41 | "url": "git+https://github.com/ezranbayantemur/react-native-rtmp-publisher.git"
42 | },
43 | "author": "Ezran (https://github.com/ezranbayantemur)",
44 | "license": "MIT",
45 | "bugs": {
46 | "url": "https://github.com/ezranbayantemur/react-native-rtmp-publisher/issues"
47 | },
48 | "homepage": "https://github.com/ezranbayantemur/react-native-rtmp-publisher#readme",
49 | "publishConfig": {
50 | "registry": "https://registry.npmjs.org/"
51 | },
52 | "devDependencies": {
53 | "@commitlint/config-conventional": "^11.0.0",
54 | "@react-native-community/eslint-config": "^2.0.0",
55 | "@release-it/conventional-changelog": "^2.0.0",
56 | "@testing-library/react-native": "^11.5.0",
57 | "@types/jest": "^26.0.0",
58 | "@types/react": "^16.9.19",
59 | "@types/react-native": "0.62.13",
60 | "commitlint": "^11.0.0",
61 | "dotenv-cli": "^4.1.1",
62 | "eslint": "^7.2.0",
63 | "eslint-config-prettier": "^7.0.0",
64 | "eslint-plugin-prettier": "^3.1.3",
65 | "husky": "^6.0.0",
66 | "jest": "^26.0.1",
67 | "pod-install": "^0.1.0",
68 | "prettier": "^2.0.5",
69 | "react": "16.13.1",
70 | "react-native": "0.63.4",
71 | "react-native-builder-bob": "^0.18.0",
72 | "react-test-renderer": "17.0.2",
73 | "release-it": "^14.2.2",
74 | "typescript": "^4.1.3"
75 | },
76 | "peerDependencies": {
77 | "react": "*",
78 | "react-native": "*"
79 | },
80 | "jest": {
81 | "preset": "react-native",
82 | "modulePathIgnorePatterns": [
83 | "/example/node_modules",
84 | "/lib/"
85 | ]
86 | },
87 | "commitlint": {
88 | "extends": [
89 | "@commitlint/config-conventional"
90 | ]
91 | },
92 | "release-it": {
93 | "git": {
94 | "commitMessage": "chore: release ${version}",
95 | "tagName": "v${version}"
96 | },
97 | "npm": {
98 | "publish": true
99 | },
100 | "github": {
101 | "release": true
102 | },
103 | "plugins": {
104 | "@release-it/conventional-changelog": {
105 | "preset": "angular"
106 | }
107 | }
108 | },
109 | "eslintConfig": {
110 | "root": true,
111 | "extends": [
112 | "@react-native-community",
113 | "prettier"
114 | ],
115 | "rules": {
116 | "prettier/prettier": [
117 | "error",
118 | {
119 | "quoteProps": "consistent",
120 | "singleQuote": true,
121 | "tabWidth": 2,
122 | "trailingComma": "es5",
123 | "useTabs": false
124 | }
125 | ]
126 | }
127 | },
128 | "eslintIgnore": [
129 | "node_modules/",
130 | "lib/"
131 | ],
132 | "prettier": {
133 | "quoteProps": "consistent",
134 | "singleQuote": true,
135 | "tabWidth": 2,
136 | "trailingComma": "es5",
137 | "useTabs": false
138 | },
139 | "react-native-builder-bob": {
140 | "source": "src",
141 | "output": "lib",
142 | "targets": [
143 | "commonjs",
144 | "module",
145 | [
146 | "typescript",
147 | {
148 | "project": "tsconfig.build.json"
149 | }
150 | ]
151 | ]
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/example/ios/RtmpExample/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/android/src/main/java/com/reactnativertmppublisher/RTMPModule.java:
--------------------------------------------------------------------------------
1 | package com.reactnativertmppublisher;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.annotation.Nullable;
5 |
6 | import com.facebook.react.bridge.Promise;
7 | import com.facebook.react.bridge.ReactMethod;
8 | import com.facebook.react.bridge.ReactApplicationContext;
9 | import com.facebook.react.bridge.ReactContextBaseJavaModule;
10 | import com.reactnativertmppublisher.enums.AudioInputType;
11 |
12 | public class RTMPModule extends ReactContextBaseJavaModule {
13 | private final String REACT_MODULE_NAME = "RTMPPublisher";
14 |
15 | public RTMPModule(@Nullable ReactApplicationContext reactContext) {
16 | super(reactContext);
17 | }
18 |
19 | @NonNull
20 | @Override
21 | public String getName() {
22 | return REACT_MODULE_NAME;
23 | }
24 |
25 | @ReactMethod
26 | public void isStreaming(Promise promise) {
27 | try {
28 | boolean streamStatus = RTMPManager.publisher.isStreaming();
29 | promise.resolve(streamStatus);
30 | } catch (Exception e) {
31 | promise.reject(e);
32 | }
33 | }
34 |
35 | @ReactMethod
36 | public void isCameraOnPreview(Promise promise) {
37 | try {
38 | boolean streamStatus = RTMPManager.publisher.isOnPreview();
39 | promise.resolve(streamStatus);
40 | } catch (Exception e) {
41 | promise.reject(e);
42 | }
43 | }
44 |
45 | @ReactMethod
46 | public void getPublishURL(Promise promise) {
47 | try {
48 | String url = RTMPManager.publisher.getPublishURL();
49 | promise.resolve(url);
50 | } catch (Exception e) {
51 | promise.reject(e);
52 | }
53 | }
54 |
55 | @ReactMethod
56 | public void hasCongestion(Promise promise) {
57 | try {
58 | boolean congestionStatus = RTMPManager.publisher.hasCongestion();
59 | promise.resolve(congestionStatus);
60 | } catch (Exception e) {
61 | promise.reject(e);
62 | }
63 | }
64 |
65 | @ReactMethod
66 | public void isAudioPrepared(Promise promise) {
67 | try {
68 | boolean status = RTMPManager.publisher.isAudioPrepared();
69 | promise.resolve(status);
70 | } catch (Exception e) {
71 | promise.reject(e);
72 | }
73 | }
74 |
75 | @ReactMethod
76 | public void isVideoPrepared(Promise promise) {
77 | try {
78 | boolean status = RTMPManager.publisher.isVideoPrepared();
79 | promise.resolve(status);
80 | } catch (Exception e) {
81 | promise.reject(e);
82 | }
83 | }
84 |
85 | @ReactMethod
86 | public void isMuted(Promise promise) {
87 | try {
88 | boolean status = RTMPManager.publisher.isAudioMuted();
89 | promise.resolve(status);
90 | } catch (Exception e) {
91 | promise.reject(e);
92 | }
93 | }
94 |
95 | @ReactMethod
96 | public void mute(Promise promise) {
97 | try {
98 | if (RTMPManager.publisher.isAudioMuted()) {
99 | return;
100 | }
101 |
102 | RTMPManager.publisher.disableAudio();
103 | } catch (Exception e) {
104 | promise.reject(e);
105 | }
106 | }
107 |
108 | @ReactMethod
109 | public void unmute(Promise promise) {
110 | try {
111 | if (!RTMPManager.publisher.isAudioMuted()) {
112 | return;
113 | }
114 |
115 | RTMPManager.publisher.enableAudio();
116 | } catch (Exception e) {
117 | promise.reject(e);
118 | }
119 | }
120 |
121 | @ReactMethod
122 | public void switchCamera(Promise promise) {
123 | try {
124 | RTMPManager.publisher.switchCamera();
125 | } catch (Exception e) {
126 | promise.reject(e);
127 | }
128 | }
129 |
130 | @ReactMethod
131 | public void startStream(Promise promise) {
132 | try {
133 | RTMPManager.publisher.startStream();
134 | } catch (Exception e) {
135 | promise.reject(e);
136 | }
137 | }
138 |
139 | @ReactMethod
140 | public void stopStream(Promise promise) {
141 | try {
142 | RTMPManager.publisher.stopStream();
143 | } catch (Exception e) {
144 | promise.reject(e);
145 | }
146 | }
147 |
148 | @ReactMethod
149 | public void toggleFlash(Promise promise) {
150 | try {
151 | RTMPManager.publisher.toggleFlash();
152 | } catch (Exception e) {
153 | promise.reject(e);
154 | }
155 | }
156 |
157 | @ReactMethod
158 | public void setAudioInput(int audioInputType, Promise promise) {
159 | try {
160 | AudioInputType selectedType = AudioInputType.values()[audioInputType];
161 | RTMPManager.publisher.setAudioInput(selectedType);
162 | } catch (Exception e) {
163 | promise.reject(e);
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/com/example/reactnativertmp/newarchitecture/MainApplicationReactNativeHost.java:
--------------------------------------------------------------------------------
1 | package com.example.reactnativertmp.newarchitecture;
2 |
3 | import android.app.Application;
4 | import androidx.annotation.NonNull;
5 | import com.facebook.react.PackageList;
6 | import com.facebook.react.ReactInstanceManager;
7 | import com.facebook.react.ReactNativeHost;
8 | import com.facebook.react.ReactPackage;
9 | import com.facebook.react.ReactPackageTurboModuleManagerDelegate;
10 | import com.facebook.react.bridge.JSIModulePackage;
11 | import com.facebook.react.bridge.JSIModuleProvider;
12 | import com.facebook.react.bridge.JSIModuleSpec;
13 | import com.facebook.react.bridge.JSIModuleType;
14 | import com.facebook.react.bridge.JavaScriptContextHolder;
15 | import com.facebook.react.bridge.ReactApplicationContext;
16 | import com.facebook.react.bridge.UIManager;
17 | import com.facebook.react.fabric.ComponentFactory;
18 | import com.facebook.react.fabric.CoreComponentsRegistry;
19 | import com.facebook.react.fabric.FabricJSIModuleProvider;
20 | import com.facebook.react.fabric.ReactNativeConfig;
21 | import com.facebook.react.uimanager.ViewManagerRegistry;
22 | import com.example.reactnativertmp.BuildConfig;
23 | import com.example.reactnativertmp.newarchitecture.components.MainComponentsRegistry;
24 | import com.example.reactnativertmp.newarchitecture.modules.MainApplicationTurboModuleManagerDelegate;
25 | import java.util.ArrayList;
26 | import java.util.List;
27 |
28 | /**
29 | * A {@link ReactNativeHost} that helps you load everything needed for the New Architecture, both
30 | * TurboModule delegates and the Fabric Renderer.
31 | *
32 | * Please note that this class is used ONLY if you opt-in for the New Architecture (see the
33 | * `newArchEnabled` property). Is ignored otherwise.
34 | */
35 | public class MainApplicationReactNativeHost extends ReactNativeHost {
36 | public MainApplicationReactNativeHost(Application application) {
37 | super(application);
38 | }
39 |
40 | @Override
41 | public boolean getUseDeveloperSupport() {
42 | return BuildConfig.DEBUG;
43 | }
44 |
45 | @Override
46 | protected List getPackages() {
47 | List packages = new PackageList(this).getPackages();
48 | // Packages that cannot be autolinked yet can be added manually here, for example:
49 | // packages.add(new MyReactNativePackage());
50 | // TurboModules must also be loaded here providing a valid TurboReactPackage implementation:
51 | // packages.add(new TurboReactPackage() { ... });
52 | // If you have custom Fabric Components, their ViewManagers should also be loaded here
53 | // inside a ReactPackage.
54 | return packages;
55 | }
56 |
57 | @Override
58 | protected String getJSMainModuleName() {
59 | return "index";
60 | }
61 |
62 | @NonNull
63 | @Override
64 | protected ReactPackageTurboModuleManagerDelegate.Builder
65 | getReactPackageTurboModuleManagerDelegateBuilder() {
66 | // Here we provide the ReactPackageTurboModuleManagerDelegate Builder. This is necessary
67 | // for the new architecture and to use TurboModules correctly.
68 | return new MainApplicationTurboModuleManagerDelegate.Builder();
69 | }
70 |
71 | @Override
72 | protected JSIModulePackage getJSIModulePackage() {
73 | return new JSIModulePackage() {
74 | @Override
75 | public List getJSIModules(
76 | final ReactApplicationContext reactApplicationContext,
77 | final JavaScriptContextHolder jsContext) {
78 | final List specs = new ArrayList<>();
79 |
80 | // Here we provide a new JSIModuleSpec that will be responsible of providing the
81 | // custom Fabric Components.
82 | specs.add(
83 | new JSIModuleSpec() {
84 | @Override
85 | public JSIModuleType getJSIModuleType() {
86 | return JSIModuleType.UIManager;
87 | }
88 |
89 | @Override
90 | public JSIModuleProvider getJSIModuleProvider() {
91 | final ComponentFactory componentFactory = new ComponentFactory();
92 | CoreComponentsRegistry.register(componentFactory);
93 |
94 | // Here we register a Components Registry.
95 | // The one that is generated with the template contains no components
96 | // and just provides you the one from React Native core.
97 | MainComponentsRegistry.register(componentFactory);
98 |
99 | final ReactInstanceManager reactInstanceManager = getReactInstanceManager();
100 |
101 | ViewManagerRegistry viewManagerRegistry =
102 | new ViewManagerRegistry(
103 | reactInstanceManager.getOrCreateViewManagers(reactApplicationContext));
104 |
105 | return new FabricJSIModuleProvider(
106 | reactApplicationContext,
107 | componentFactory,
108 | ReactNativeConfig.DEFAULT_CONFIG,
109 | viewManagerRegistry);
110 | }
111 | });
112 | return specs;
113 | }
114 | };
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/RTMPPublisher.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, useImperativeHandle } from 'react';
2 | import { NativeModules, type ViewStyle } from 'react-native';
3 | import PublisherComponent, {
4 | type DisconnectType,
5 | type ConnectionFailedType,
6 | type ConnectionStartedType,
7 | type ConnectionSuccessType,
8 | type NewBitrateReceivedType,
9 | type StreamStateChangedType,
10 | type BluetoothDeviceStatusChangedType,
11 | } from './Component';
12 | import type {
13 | RTMPPublisherRefProps,
14 | StreamState,
15 | BluetoothDeviceStatuses,
16 | AudioInputType,
17 | } from './types';
18 |
19 | const RTMPModule = NativeModules.RTMPPublisher;
20 | export interface RTMPPublisherProps {
21 | testID?: string;
22 | style?: ViewStyle;
23 | streamURL: string;
24 | streamName: string;
25 | /**
26 | * Callback for connection fails on RTMP server
27 | */
28 | onConnectionFailed?: (data: string) => void;
29 | /**
30 | * Callback for starting connection to RTMP server
31 | */
32 | onConnectionStarted?: (data: string) => void;
33 | /**
34 | * Callback for connection successfully to RTMP server
35 | */
36 | onConnectionSuccess?: (data: null) => void;
37 | /**
38 | * Callback for disconnect successfully to RTMP server
39 | */
40 | onDisconnect?: (data: null) => void;
41 | /**
42 | * Callback for receiving new bitrate value about stream
43 | */
44 | onNewBitrateReceived?: (data: number) => void;
45 | /**
46 | * Alternatively callback for changing stream state
47 | * Returns parameter StreamState type
48 | */
49 | onStreamStateChanged?: (data: StreamState) => void;
50 | /**
51 | * Callback for bluetooth device connection changes
52 | */
53 | onBluetoothDeviceStatusChanged?: (data: BluetoothDeviceStatuses) => void;
54 | }
55 |
56 | const RTMPPublisher = forwardRef(
57 | (
58 | {
59 | onConnectionFailed,
60 | onConnectionStarted,
61 | onConnectionSuccess,
62 | onDisconnect,
63 | onNewBitrateReceived,
64 | onStreamStateChanged,
65 | onBluetoothDeviceStatusChanged,
66 | ...props
67 | },
68 | ref
69 | ) => {
70 | const startStream = async () => await RTMPModule.startStream();
71 |
72 | const stopStream = async () => await RTMPModule.stopStream();
73 |
74 | const isStreaming = async () => RTMPModule.isStreaming();
75 |
76 | const isCameraOnPreview = async () => RTMPModule.isCameraOnPreview();
77 |
78 | const getPublishURL = async () => RTMPModule.getPublishURL();
79 |
80 | const hasCongestion = async () => RTMPModule.hasCongestion();
81 |
82 | const isAudioPrepared = async () => RTMPModule.isAudioPrepared();
83 |
84 | const isVideoPrepared = async () => RTMPModule.isVideoPrepared();
85 |
86 | const isMuted = async () => RTMPModule.isMuted();
87 |
88 | const mute = () => RTMPModule.mute();
89 |
90 | const unmute = () => RTMPModule.unmute();
91 |
92 | const switchCamera = () => RTMPModule.switchCamera();
93 |
94 | const toggleFlash = () => RTMPModule.toggleFlash();
95 |
96 | const setAudioInput = (audioInput: AudioInputType) =>
97 | RTMPModule.setAudioInput(audioInput);
98 |
99 | const handleOnConnectionFailed = (e: ConnectionFailedType) => {
100 | onConnectionFailed && onConnectionFailed(e.nativeEvent.data);
101 | };
102 |
103 | const handleOnConnectionStarted = (e: ConnectionStartedType) => {
104 | onConnectionStarted && onConnectionStarted(e.nativeEvent.data);
105 | };
106 |
107 | const handleOnConnectionSuccess = (e: ConnectionSuccessType) => {
108 | onConnectionSuccess && onConnectionSuccess(e.nativeEvent.data);
109 | };
110 |
111 | const handleOnDisconnect = (e: DisconnectType) => {
112 | onDisconnect && onDisconnect(e.nativeEvent.data);
113 | };
114 |
115 | const handleOnNewBitrateReceived = (e: NewBitrateReceivedType) => {
116 | onNewBitrateReceived && onNewBitrateReceived(e.nativeEvent.data);
117 | };
118 |
119 | const handleOnStreamStateChanged = (e: StreamStateChangedType) => {
120 | onStreamStateChanged && onStreamStateChanged(e.nativeEvent.data);
121 | };
122 |
123 | const handleBluetoothDeviceStatusChanged = (
124 | e: BluetoothDeviceStatusChangedType
125 | ) => {
126 | onBluetoothDeviceStatusChanged &&
127 | onBluetoothDeviceStatusChanged(e.nativeEvent.data);
128 | };
129 |
130 | useImperativeHandle(ref, () => ({
131 | startStream,
132 | stopStream,
133 | isStreaming,
134 | isCameraOnPreview,
135 | getPublishURL,
136 | hasCongestion,
137 | isAudioPrepared,
138 | isVideoPrepared,
139 | isMuted,
140 | mute,
141 | unmute,
142 | switchCamera,
143 | toggleFlash,
144 | setAudioInput,
145 | }));
146 |
147 | return (
148 |
158 | );
159 | }
160 | );
161 |
162 | export default RTMPPublisher;
163 |
--------------------------------------------------------------------------------
/example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from 'react';
2 |
3 | import { View, Platform } from 'react-native';
4 | import RTMPPublisher, {
5 | RTMPPublisherRefProps,
6 | StreamState,
7 | AudioInputType,
8 | BluetoothDeviceStatuses,
9 | } from 'react-native-rtmp-publisher';
10 |
11 | import styles from './App.styles';
12 |
13 | import Button from './components/Button';
14 | import LiveBadge from './components/LiveBadge';
15 | import usePermissions from './hooks/usePermissions';
16 | import MicrophoneSelectModal from './components/MicrophoneSelectModal';
17 |
18 | const STREAM_URL = 'YOUR_STREAM_URL'; // ex: rtmp://a.rtmp.youtube.com/live2
19 | const STREAM_NAME = 'YOUR_STREAM_NAME'; // ex: abcd-1234-abcd-1234-abcd
20 |
21 | export default function App() {
22 | const publisherRef = useRef(null);
23 | const [isStreaming, setIsStreaming] = useState(false);
24 | const [isMuted, setIsMuted] = useState(false);
25 | const [hasBluetoothDevice, setHasBluetoothDevice] = useState(false);
26 | const [microphoneModalVisibility, setMicrophoneModalVisibility] =
27 | useState(false);
28 |
29 | const { permissionGranted } = usePermissions();
30 |
31 | const handleOnConnectionFailed = (data: String) => {
32 | console.log('Connection Failed: ' + data);
33 | };
34 |
35 | const handleOnConnectionStarted = (data: String) => {
36 | console.log('Connection Started: ' + data);
37 | };
38 |
39 | const handleOnConnectionSuccess = () => {
40 | console.log('Connected');
41 | setIsStreaming(true);
42 | };
43 |
44 | const handleOnDisconnect = () => {
45 | console.log('Disconnected');
46 | setIsStreaming(false);
47 | };
48 |
49 | const handleOnNewBitrateReceived = (data: number) => {
50 | console.log('New Bitrate Received: ' + data);
51 | };
52 |
53 | const handleOnStreamStateChanged = (data: StreamState) => {
54 | console.log('Stream Status: ' + data);
55 | };
56 |
57 | const handleUnmute = () => {
58 | publisherRef.current && publisherRef.current.unmute();
59 | setIsMuted(false);
60 | };
61 |
62 | const handleMute = () => {
63 | publisherRef.current && publisherRef.current.mute();
64 | setIsMuted(true);
65 | };
66 |
67 | const handleStartStream = () => {
68 | publisherRef.current && publisherRef.current.startStream();
69 | };
70 |
71 | const handleStopStream = () => {
72 | publisherRef.current && publisherRef.current.stopStream();
73 | };
74 |
75 | const handleSwitchCamera = () => {
76 | publisherRef.current && publisherRef.current.switchCamera();
77 | };
78 |
79 | const handleToggleMicrophoneModal = () => {
80 | setMicrophoneModalVisibility(!microphoneModalVisibility);
81 | };
82 |
83 | const handleMicrophoneSelect = (selectedMicrophone: AudioInputType) => {
84 | publisherRef.current &&
85 | publisherRef.current.setAudioInput(selectedMicrophone);
86 | };
87 |
88 | const handleBluetoothDeviceStatusChange = (
89 | status: BluetoothDeviceStatuses
90 | ) => {
91 | switch (status) {
92 | case BluetoothDeviceStatuses.CONNECTED: {
93 | setHasBluetoothDevice(true);
94 | break;
95 | }
96 |
97 | case BluetoothDeviceStatuses.DISCONNECTED: {
98 | setHasBluetoothDevice(false);
99 | break;
100 | }
101 |
102 | default:
103 | break;
104 | }
105 | };
106 |
107 | return (
108 |
109 | {permissionGranted && (
110 |
123 | )}
124 |
125 |
126 | {isMuted ? (
127 |
128 | ) : (
129 |
130 | )}
131 |
132 |
133 | {isStreaming ? (
134 |
135 | ) : (
136 |
137 | )}
138 |
139 |
140 |
141 | {(Platform.OS === 'ios' || hasBluetoothDevice) && (
142 |
147 | )}
148 |
149 |
150 | {isStreaming && }
151 |
156 |
157 | );
158 | }
159 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | .
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/example/ios/RtmpExample/AppDelegate.mm:
--------------------------------------------------------------------------------
1 | #import "AppDelegate.h"
2 |
3 | #import
4 | #import
5 | #import
6 |
7 | #import
8 | #import
9 |
10 | #if RCT_NEW_ARCH_ENABLED
11 | #import
12 | #import
13 | #import
14 | #import
15 | #import
16 | #import
17 |
18 | #import
19 |
20 | static NSString *const kRNConcurrentRoot = @"concurrentRoot";
21 |
22 | @interface AppDelegate () {
23 | RCTTurboModuleManager *_turboModuleManager;
24 | RCTSurfacePresenterBridgeAdapter *_bridgeAdapter;
25 | std::shared_ptr _reactNativeConfig;
26 | facebook::react::ContextContainer::Shared _contextContainer;
27 | }
28 | @end
29 | #endif
30 |
31 | @implementation AppDelegate
32 |
33 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
34 | {
35 | RCTAppSetupPrepareApp(application);
36 |
37 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
38 |
39 | #if RCT_NEW_ARCH_ENABLED
40 | _contextContainer = std::make_shared();
41 | _reactNativeConfig = std::make_shared();
42 | _contextContainer->insert("ReactNativeConfig", _reactNativeConfig);
43 | _bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer];
44 | bridge.surfacePresenter = _bridgeAdapter.surfacePresenter;
45 | #endif
46 |
47 | NSDictionary *initProps = [self prepareInitialProps];
48 | UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"RtmpExample", initProps);
49 |
50 | if (@available(iOS 13.0, *)) {
51 | rootView.backgroundColor = [UIColor systemBackgroundColor];
52 | } else {
53 | rootView.backgroundColor = [UIColor whiteColor];
54 | }
55 |
56 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
57 | UIViewController *rootViewController = [UIViewController new];
58 | rootViewController.view = rootView;
59 | self.window.rootViewController = rootViewController;
60 | [self.window makeKeyAndVisible];
61 |
62 |
63 | // Implementation for bluetooth headset
64 | AVAudioSession *session = AVAudioSession.sharedInstance;
65 | NSError *error = nil;
66 |
67 | if (@available(iOS 10.0, *)) {
68 | [session
69 | setCategory:AVAudioSessionCategoryPlayAndRecord
70 | mode:AVAudioSessionModeVoiceChat
71 | options:AVAudioSessionCategoryOptionDefaultToSpeaker|AVAudioSessionCategoryOptionAllowBluetooth
72 | error:&error];
73 | } else {
74 | SEL selector = NSSelectorFromString(@"setCategory:withOptions:error:");
75 |
76 | NSArray * optionsArray =
77 | [NSArray arrayWithObjects:
78 | [NSNumber numberWithInteger:AVAudioSessionCategoryOptionAllowBluetooth],
79 | [NSNumber numberWithInteger:AVAudioSessionCategoryOptionDefaultToSpeaker], nil];
80 |
81 | [session
82 | performSelector:selector
83 | withObject: AVAudioSessionCategoryPlayAndRecord
84 | withObject: optionsArray
85 | ];
86 |
87 | [session setMode:AVAudioSessionModeVoiceChat error:&error];
88 | }
89 |
90 | [session setActive: YES error:&error];
91 | // Implementation for bluetooth headset
92 |
93 | return YES;
94 | }
95 |
96 | /// This method controls whether the `concurrentRoot`feature of React18 is turned on or off.
97 | ///
98 | /// @see: https://reactjs.org/blog/2022/03/29/react-v18.html
99 | /// @note: This requires to be rendering on Fabric (i.e. on the New Architecture).
100 | /// @return: `true` if the `concurrentRoot` feture is enabled. Otherwise, it returns `false`.
101 | - (BOOL)concurrentRootEnabled
102 | {
103 | // Switch this bool to turn on and off the concurrent root
104 | return true;
105 | }
106 |
107 | - (NSDictionary *)prepareInitialProps
108 | {
109 | NSMutableDictionary *initProps = [NSMutableDictionary new];
110 |
111 | #ifdef RCT_NEW_ARCH_ENABLED
112 | initProps[kRNConcurrentRoot] = @([self concurrentRootEnabled]);
113 | #endif
114 |
115 | return initProps;
116 | }
117 |
118 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
119 | {
120 | #if DEBUG
121 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
122 | #else
123 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
124 | #endif
125 | }
126 |
127 | #if RCT_NEW_ARCH_ENABLED
128 |
129 | #pragma mark - RCTCxxBridgeDelegate
130 |
131 | - (std::unique_ptr)jsExecutorFactoryForBridge:(RCTBridge *)bridge
132 | {
133 | _turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge
134 | delegate:self
135 | jsInvoker:bridge.jsCallInvoker];
136 | return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager);
137 | }
138 |
139 | #pragma mark RCTTurboModuleManagerDelegate
140 |
141 | - (Class)getModuleClassFromName:(const char *)name
142 | {
143 | return RCTCoreModulesClassProvider(name);
144 | }
145 |
146 | - (std::shared_ptr)getTurboModule:(const std::string &)name
147 | jsInvoker:(std::shared_ptr)jsInvoker
148 | {
149 | return nullptr;
150 | }
151 |
152 | - (std::shared_ptr)getTurboModule:(const std::string &)name
153 | initParams:
154 | (const facebook::react::ObjCTurboModule::InitParams &)params
155 | {
156 | return nullptr;
157 | }
158 |
159 | - (id)getModuleInstanceFromClass:(Class)moduleClass
160 | {
161 | return RCTAppSetupDefaultModuleFromClass(moduleClass);
162 | }
163 |
164 | #endif
165 |
166 | @end
167 |
--------------------------------------------------------------------------------
/coverage/lcov-report/base.css:
--------------------------------------------------------------------------------
1 | body, html {
2 | margin:0; padding: 0;
3 | height: 100%;
4 | }
5 | body {
6 | font-family: Helvetica Neue, Helvetica, Arial;
7 | font-size: 14px;
8 | color:#333;
9 | }
10 | .small { font-size: 12px; }
11 | *, *:after, *:before {
12 | -webkit-box-sizing:border-box;
13 | -moz-box-sizing:border-box;
14 | box-sizing:border-box;
15 | }
16 | h1 { font-size: 20px; margin: 0;}
17 | h2 { font-size: 14px; }
18 | pre {
19 | font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
20 | margin: 0;
21 | padding: 0;
22 | -moz-tab-size: 2;
23 | -o-tab-size: 2;
24 | tab-size: 2;
25 | }
26 | a { color:#0074D9; text-decoration:none; }
27 | a:hover { text-decoration:underline; }
28 | .strong { font-weight: bold; }
29 | .space-top1 { padding: 10px 0 0 0; }
30 | .pad2y { padding: 20px 0; }
31 | .pad1y { padding: 10px 0; }
32 | .pad2x { padding: 0 20px; }
33 | .pad2 { padding: 20px; }
34 | .pad1 { padding: 10px; }
35 | .space-left2 { padding-left:55px; }
36 | .space-right2 { padding-right:20px; }
37 | .center { text-align:center; }
38 | .clearfix { display:block; }
39 | .clearfix:after {
40 | content:'';
41 | display:block;
42 | height:0;
43 | clear:both;
44 | visibility:hidden;
45 | }
46 | .fl { float: left; }
47 | @media only screen and (max-width:640px) {
48 | .col3 { width:100%; max-width:100%; }
49 | .hide-mobile { display:none!important; }
50 | }
51 |
52 | .quiet {
53 | color: #7f7f7f;
54 | color: rgba(0,0,0,0.5);
55 | }
56 | .quiet a { opacity: 0.7; }
57 |
58 | .fraction {
59 | font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
60 | font-size: 10px;
61 | color: #555;
62 | background: #E8E8E8;
63 | padding: 4px 5px;
64 | border-radius: 3px;
65 | vertical-align: middle;
66 | }
67 |
68 | div.path a:link, div.path a:visited { color: #333; }
69 | table.coverage {
70 | border-collapse: collapse;
71 | margin: 10px 0 0 0;
72 | padding: 0;
73 | }
74 |
75 | table.coverage td {
76 | margin: 0;
77 | padding: 0;
78 | vertical-align: top;
79 | }
80 | table.coverage td.line-count {
81 | text-align: right;
82 | padding: 0 5px 0 20px;
83 | }
84 | table.coverage td.line-coverage {
85 | text-align: right;
86 | padding-right: 10px;
87 | min-width:20px;
88 | }
89 |
90 | table.coverage td span.cline-any {
91 | display: inline-block;
92 | padding: 0 5px;
93 | width: 100%;
94 | }
95 | .missing-if-branch {
96 | display: inline-block;
97 | margin-right: 5px;
98 | border-radius: 3px;
99 | position: relative;
100 | padding: 0 4px;
101 | background: #333;
102 | color: yellow;
103 | }
104 |
105 | .skip-if-branch {
106 | display: none;
107 | margin-right: 10px;
108 | position: relative;
109 | padding: 0 4px;
110 | background: #ccc;
111 | color: white;
112 | }
113 | .missing-if-branch .typ, .skip-if-branch .typ {
114 | color: inherit !important;
115 | }
116 | .coverage-summary {
117 | border-collapse: collapse;
118 | width: 100%;
119 | }
120 | .coverage-summary tr { border-bottom: 1px solid #bbb; }
121 | .keyline-all { border: 1px solid #ddd; }
122 | .coverage-summary td, .coverage-summary th { padding: 10px; }
123 | .coverage-summary tbody { border: 1px solid #bbb; }
124 | .coverage-summary td { border-right: 1px solid #bbb; }
125 | .coverage-summary td:last-child { border-right: none; }
126 | .coverage-summary th {
127 | text-align: left;
128 | font-weight: normal;
129 | white-space: nowrap;
130 | }
131 | .coverage-summary th.file { border-right: none !important; }
132 | .coverage-summary th.pct { }
133 | .coverage-summary th.pic,
134 | .coverage-summary th.abs,
135 | .coverage-summary td.pct,
136 | .coverage-summary td.abs { text-align: right; }
137 | .coverage-summary td.file { white-space: nowrap; }
138 | .coverage-summary td.pic { min-width: 120px !important; }
139 | .coverage-summary tfoot td { }
140 |
141 | .coverage-summary .sorter {
142 | height: 10px;
143 | width: 7px;
144 | display: inline-block;
145 | margin-left: 0.5em;
146 | background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
147 | }
148 | .coverage-summary .sorted .sorter {
149 | background-position: 0 -20px;
150 | }
151 | .coverage-summary .sorted-desc .sorter {
152 | background-position: 0 -10px;
153 | }
154 | .status-line { height: 10px; }
155 | /* yellow */
156 | .cbranch-no { background: yellow !important; color: #111; }
157 | /* dark red */
158 | .red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
159 | .low .chart { border:1px solid #C21F39 }
160 | .highlighted,
161 | .highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
162 | background: #C21F39 !important;
163 | }
164 | /* medium red */
165 | .cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
166 | /* light red */
167 | .low, .cline-no { background:#FCE1E5 }
168 | /* light green */
169 | .high, .cline-yes { background:rgb(230,245,208) }
170 | /* medium green */
171 | .cstat-yes { background:rgb(161,215,106) }
172 | /* dark green */
173 | .status-line.high, .high .cover-fill { background:rgb(77,146,33) }
174 | .high .chart { border:1px solid rgb(77,146,33) }
175 | /* dark yellow (gold) */
176 | .status-line.medium, .medium .cover-fill { background: #f9cd0b; }
177 | .medium .chart { border:1px solid #f9cd0b; }
178 | /* light yellow */
179 | .medium { background: #fff4c2; }
180 |
181 | .cstat-skip { background: #ddd; color: #111; }
182 | .fstat-skip { background: #ddd; color: #111 !important; }
183 | .cbranch-skip { background: #ddd !important; color: #111; }
184 |
185 | span.cline-neutral { background: #eaeaea; }
186 |
187 | .coverage-summary td.empty {
188 | opacity: .5;
189 | padding-top: 4px;
190 | padding-bottom: 4px;
191 | line-height: 1;
192 | color: #888;
193 | }
194 |
195 | .cover-fill, .cover-empty {
196 | display:inline-block;
197 | height: 12px;
198 | }
199 | .chart {
200 | line-height: 0;
201 | }
202 | .cover-empty {
203 | background: white;
204 | }
205 | .cover-full {
206 | border-right: none !important;
207 | }
208 | pre.prettyprint {
209 | border: none !important;
210 | padding: 0 !important;
211 | margin: 0 !important;
212 | }
213 | .com { color: #999 !important; }
214 | .ignore-none { color: #999; font-weight: normal; }
215 |
216 | .wrapper {
217 | min-height: 100%;
218 | height: auto !important;
219 | height: 100%;
220 | margin: 0 auto -48px;
221 | }
222 | .footer, .push {
223 | height: 48px;
224 | }
225 |
--------------------------------------------------------------------------------
/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/coverage/lcov-report/sorter.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var addSorting = (function() {
3 | 'use strict';
4 | var cols,
5 | currentSort = {
6 | index: 0,
7 | desc: false
8 | };
9 |
10 | // returns the summary table element
11 | function getTable() {
12 | return document.querySelector('.coverage-summary');
13 | }
14 | // returns the thead element of the summary table
15 | function getTableHeader() {
16 | return getTable().querySelector('thead tr');
17 | }
18 | // returns the tbody element of the summary table
19 | function getTableBody() {
20 | return getTable().querySelector('tbody');
21 | }
22 | // returns the th element for nth column
23 | function getNthColumn(n) {
24 | return getTableHeader().querySelectorAll('th')[n];
25 | }
26 |
27 | function onFilterInput() {
28 | const searchValue = document.getElementById('fileSearch').value;
29 | const rows = document.getElementsByTagName('tbody')[0].children;
30 | for (let i = 0; i < rows.length; i++) {
31 | const row = rows[i];
32 | if (
33 | row.textContent
34 | .toLowerCase()
35 | .includes(searchValue.toLowerCase())
36 | ) {
37 | row.style.display = '';
38 | } else {
39 | row.style.display = 'none';
40 | }
41 | }
42 | }
43 |
44 | // loads the search box
45 | function addSearchBox() {
46 | var template = document.getElementById('filterTemplate');
47 | var templateClone = template.content.cloneNode(true);
48 | templateClone.getElementById('fileSearch').oninput = onFilterInput;
49 | template.parentElement.appendChild(templateClone);
50 | }
51 |
52 | // loads all columns
53 | function loadColumns() {
54 | var colNodes = getTableHeader().querySelectorAll('th'),
55 | colNode,
56 | cols = [],
57 | col,
58 | i;
59 |
60 | for (i = 0; i < colNodes.length; i += 1) {
61 | colNode = colNodes[i];
62 | col = {
63 | key: colNode.getAttribute('data-col'),
64 | sortable: !colNode.getAttribute('data-nosort'),
65 | type: colNode.getAttribute('data-type') || 'string'
66 | };
67 | cols.push(col);
68 | if (col.sortable) {
69 | col.defaultDescSort = col.type === 'number';
70 | colNode.innerHTML =
71 | colNode.innerHTML + '';
72 | }
73 | }
74 | return cols;
75 | }
76 | // attaches a data attribute to every tr element with an object
77 | // of data values keyed by column name
78 | function loadRowData(tableRow) {
79 | var tableCols = tableRow.querySelectorAll('td'),
80 | colNode,
81 | col,
82 | data = {},
83 | i,
84 | val;
85 | for (i = 0; i < tableCols.length; i += 1) {
86 | colNode = tableCols[i];
87 | col = cols[i];
88 | val = colNode.getAttribute('data-value');
89 | if (col.type === 'number') {
90 | val = Number(val);
91 | }
92 | data[col.key] = val;
93 | }
94 | return data;
95 | }
96 | // loads all row data
97 | function loadData() {
98 | var rows = getTableBody().querySelectorAll('tr'),
99 | i;
100 |
101 | for (i = 0; i < rows.length; i += 1) {
102 | rows[i].data = loadRowData(rows[i]);
103 | }
104 | }
105 | // sorts the table using the data for the ith column
106 | function sortByIndex(index, desc) {
107 | var key = cols[index].key,
108 | sorter = function(a, b) {
109 | a = a.data[key];
110 | b = b.data[key];
111 | return a < b ? -1 : a > b ? 1 : 0;
112 | },
113 | finalSorter = sorter,
114 | tableBody = document.querySelector('.coverage-summary tbody'),
115 | rowNodes = tableBody.querySelectorAll('tr'),
116 | rows = [],
117 | i;
118 |
119 | if (desc) {
120 | finalSorter = function(a, b) {
121 | return -1 * sorter(a, b);
122 | };
123 | }
124 |
125 | for (i = 0; i < rowNodes.length; i += 1) {
126 | rows.push(rowNodes[i]);
127 | tableBody.removeChild(rowNodes[i]);
128 | }
129 |
130 | rows.sort(finalSorter);
131 |
132 | for (i = 0; i < rows.length; i += 1) {
133 | tableBody.appendChild(rows[i]);
134 | }
135 | }
136 | // removes sort indicators for current column being sorted
137 | function removeSortIndicators() {
138 | var col = getNthColumn(currentSort.index),
139 | cls = col.className;
140 |
141 | cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
142 | col.className = cls;
143 | }
144 | // adds sort indicators for current column being sorted
145 | function addSortIndicators() {
146 | getNthColumn(currentSort.index).className += currentSort.desc
147 | ? ' sorted-desc'
148 | : ' sorted';
149 | }
150 | // adds event listeners for all sorter widgets
151 | function enableUI() {
152 | var i,
153 | el,
154 | ithSorter = function ithSorter(i) {
155 | var col = cols[i];
156 |
157 | return function() {
158 | var desc = col.defaultDescSort;
159 |
160 | if (currentSort.index === i) {
161 | desc = !currentSort.desc;
162 | }
163 | sortByIndex(i, desc);
164 | removeSortIndicators();
165 | currentSort.index = i;
166 | currentSort.desc = desc;
167 | addSortIndicators();
168 | };
169 | };
170 | for (i = 0; i < cols.length; i += 1) {
171 | if (cols[i].sortable) {
172 | // add the click event handler on the th so users
173 | // dont have to click on those tiny arrows
174 | el = getNthColumn(i).querySelector('.sorter').parentElement;
175 | if (el.addEventListener) {
176 | el.addEventListener('click', ithSorter(i));
177 | } else {
178 | el.attachEvent('onclick', ithSorter(i));
179 | }
180 | }
181 | }
182 | }
183 | // adds sorting functionality to the UI
184 | return function() {
185 | if (!getTable()) {
186 | return;
187 | }
188 | cols = loadColumns();
189 | loadData();
190 | addSearchBox();
191 | addSortIndicators();
192 | enableUI();
193 | };
194 | })();
195 |
196 | window.addEventListener('load', addSorting);
197 |
--------------------------------------------------------------------------------
/android/src/main/java/com/reactnativertmppublisher/modules/Publisher.java:
--------------------------------------------------------------------------------
1 | package com.reactnativertmppublisher.modules;
2 |
3 | import android.content.Context;
4 | import android.media.AudioManager;
5 | import android.media.MediaRecorder;
6 | import android.util.Log;
7 | import android.view.SurfaceView;
8 |
9 | import androidx.annotation.NonNull;
10 |
11 | import com.facebook.react.bridge.Arguments;
12 | import com.facebook.react.bridge.WritableMap;
13 | import com.facebook.react.uimanager.ThemedReactContext;
14 | import com.facebook.react.uimanager.events.RCTEventEmitter;
15 | import com.pedro.rtplibrary.rtmp.RtmpCamera1;
16 | import com.reactnativertmppublisher.enums.AudioInputType;
17 | import com.reactnativertmppublisher.enums.StreamState;
18 | import com.reactnativertmppublisher.interfaces.ConnectionListener;
19 | import com.reactnativertmppublisher.utils.ObjectCaster;
20 |
21 | public class Publisher {
22 | private final SurfaceView _surfaceView;
23 | private final RtmpCamera1 _rtmpCamera;
24 | private final ThemedReactContext _reactContext;
25 | private final AudioManager _mAudioManager;
26 | private String _streamUrl;
27 | private String _streamName;
28 | ConnectionChecker _connectionChecker = new ConnectionChecker();
29 | BluetoothDeviceConnector _bluetoothDeviceConnector;
30 |
31 | public Publisher(ThemedReactContext reactContext, SurfaceView surfaceView) {
32 | _reactContext = reactContext;
33 | _surfaceView = surfaceView;
34 | _rtmpCamera = new RtmpCamera1(surfaceView, _connectionChecker);
35 | _bluetoothDeviceConnector = new BluetoothDeviceConnector(reactContext);
36 |
37 | _bluetoothDeviceConnector.addListener(createBluetoothDeviceListener());
38 | _connectionChecker.addListener(createConnectionListener());
39 | _mAudioManager = (AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE);
40 |
41 | setAudioInput(AudioInputType.SPEAKER);
42 | }
43 |
44 | public RtmpCamera1 getRtmpCamera() {
45 | return _rtmpCamera;
46 | }
47 |
48 | public ConnectionListener createConnectionListener() {
49 | return (type, data) -> {
50 | eventEffect(type);
51 | WritableMap eventData = ObjectCaster.caster(data);
52 |
53 | _reactContext
54 | .getJSModule(RCTEventEmitter.class)
55 | .receiveEvent(_surfaceView.getId(), type, eventData);
56 | };
57 | }
58 |
59 | public ConnectionListener createBluetoothDeviceListener(){
60 | return (type, data) -> {
61 | eventEffect(type);
62 | WritableMap eventData = ObjectCaster.caster(data);
63 |
64 | _reactContext
65 | .getJSModule(RCTEventEmitter.class)
66 | .receiveEvent(_surfaceView.getId(), type, eventData);
67 | };
68 | }
69 |
70 | private void eventEffect(@NonNull String eventType) {
71 | switch (eventType) {
72 | case "onConnectionStarted": {
73 | WritableMap event = Arguments.createMap();
74 | event.putString("data", String.valueOf(StreamState.CONNECTING));
75 |
76 | _reactContext
77 | .getJSModule(RCTEventEmitter.class)
78 | .receiveEvent(_surfaceView.getId(), "onStreamStateChanged", event);
79 | break;
80 | }
81 |
82 | case "onConnectionSuccess": {
83 | WritableMap event = Arguments.createMap();
84 | event.putString("data", String.valueOf(StreamState.CONNECTED));
85 |
86 | _reactContext
87 | .getJSModule(RCTEventEmitter.class)
88 | .receiveEvent(_surfaceView.getId(), "onStreamStateChanged", event);
89 | break;
90 | }
91 |
92 | case "onDisconnect": {
93 | WritableMap event = Arguments.createMap();
94 | event.putString("data", String.valueOf(StreamState.DISCONNECTED));
95 |
96 | _reactContext
97 | .getJSModule(RCTEventEmitter.class)
98 | .receiveEvent(_surfaceView.getId(), "onStreamStateChanged", event);
99 | break;
100 | }
101 |
102 | case "onConnectionFailed": {
103 | WritableMap event = Arguments.createMap();
104 | event.putString("data", String.valueOf(StreamState.FAILED));
105 |
106 | _reactContext
107 | .getJSModule(RCTEventEmitter.class)
108 | .receiveEvent(_surfaceView.getId(), "onStreamStateChanged", event);
109 | break;
110 | }
111 | }
112 | }
113 |
114 |
115 | //region COMPONENT METHODS
116 | public String getPublishURL() {
117 | return _streamUrl + "/" + _streamName;
118 | }
119 |
120 | public void setStreamUrl(String _streamUrl) {
121 | this._streamUrl = _streamUrl;
122 | }
123 |
124 | public void setStreamName(String _streamName) {
125 | this._streamName = _streamName;
126 | }
127 |
128 | public boolean isStreaming() {
129 | return _rtmpCamera.isStreaming();
130 | }
131 |
132 | public boolean isOnPreview() {
133 | return _rtmpCamera.isOnPreview();
134 | }
135 |
136 | public boolean isAudioPrepared() {
137 | return _rtmpCamera.prepareAudio();
138 | }
139 |
140 | public boolean isVideoPrepared() {
141 | return _rtmpCamera.prepareVideo();
142 | }
143 |
144 | public boolean hasCongestion() {
145 | return _rtmpCamera.hasCongestion();
146 | }
147 |
148 | public boolean isAudioMuted() {
149 | return _rtmpCamera.isAudioMuted();
150 | }
151 |
152 | public void disableAudio() {
153 | _rtmpCamera.disableAudio();
154 | }
155 |
156 | public void enableAudio() {
157 | _rtmpCamera.enableAudio();
158 | }
159 |
160 | public void switchCamera() {
161 | _rtmpCamera.switchCamera();
162 | }
163 |
164 | public void toggleFlash() {
165 | try {
166 | if(_rtmpCamera.isLanternEnabled()){
167 | _rtmpCamera.disableLantern();
168 | return;
169 | }
170 |
171 | _rtmpCamera.enableLantern();
172 | }
173 | catch (Exception e){
174 | e.printStackTrace();
175 | }
176 | }
177 |
178 | public void startStream() {
179 | try {
180 | boolean isAudioPrepared = _rtmpCamera.prepareAudio(MediaRecorder.AudioSource.DEFAULT, 128 * 1024, 44100, true, false, false);
181 | boolean isVideoPrepared = _rtmpCamera.prepareVideo(1280 , 720, 3000 * 1024);
182 |
183 | if (!isAudioPrepared || !isVideoPrepared || _streamName == null || _streamUrl == null) {
184 | return;
185 | }
186 |
187 | String url = _streamUrl + "/" + _streamName;
188 | _rtmpCamera.startStream(url);
189 | } catch (Exception e) {
190 | e.printStackTrace();
191 | }
192 | }
193 |
194 | public void stopStream() {
195 | try {
196 | boolean isStreaming = _rtmpCamera.isStreaming();
197 |
198 | if (!isStreaming) {
199 | return;
200 | }
201 |
202 | _rtmpCamera.stopStream();
203 | } catch (Exception e) {
204 | e.printStackTrace();
205 | }
206 | }
207 |
208 | public void setAudioInput(@NonNull AudioInputType audioInputType){
209 | System.out.println(audioInputType);
210 | switch (audioInputType){
211 | case BLUETOOTH_HEADSET: {
212 | System.out.println("ble");
213 | try{
214 | _mAudioManager.startBluetoothSco();
215 | _mAudioManager.setBluetoothScoOn(true);
216 | break;
217 | }
218 | catch (Exception error){
219 | System.out.println(error);
220 | break;
221 | }
222 | }
223 |
224 | case SPEAKER:{
225 | try{
226 | if(_mAudioManager.isBluetoothScoOn()){
227 | _mAudioManager.stopBluetoothSco();
228 | _mAudioManager.setBluetoothScoOn(false);
229 | }
230 |
231 | _mAudioManager.setSpeakerphoneOn(true);
232 | break;
233 | }
234 | catch (Exception error){
235 | System.out.println(error);
236 | break;
237 | }
238 | }
239 | }
240 | }
241 | //endregion
242 |
243 | }
244 |
--------------------------------------------------------------------------------
/coverage/lcov-report/Component.tsx.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Code coverage report for Component.tsx
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 0%
27 | Statements
28 | 0/0
29 |
30 |
31 |
32 |
33 | 0%
34 | Branches
35 | 0/0
36 |
37 |
38 |
39 |
40 | 0%
41 | Functions
42 | 0/0
43 |
44 |
45 |
46 |
47 | 0%
48 | Lines
49 | 0/0
50 |
51 |
52 |
53 |
54 |
55 | Press n or j to go to the next uncovered block, b, p or k for the previous block.
56 |
57 |
58 |
59 | Filter:
60 |
61 |
62 |
63 |
64 |
65 |
66 | | 1
67 | 2
68 | 3
69 | 4
70 | 5
71 | 6
72 | 7
73 | 8
74 | 9
75 | 10
76 | 11
77 | 12
78 | 13
79 | 14
80 | 15
81 | 16
82 | 17
83 | 18
84 | 19
85 | 20
86 | 21
87 | 22
88 | 23
89 | 24
90 | 25
91 | 26
92 | 27
93 | 28
94 | 29
95 | 30
96 | 31
97 | 32
98 | 33
99 | 34
100 | 35
101 | 36
102 | 37
103 | 38 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | | import {
141 | NativeSyntheticEvent,
142 | requireNativeComponent,
143 | ViewStyle,
144 | } from 'react-native';
145 | import type { StreamState, BluetoothDeviceStatuses } from './types';
146 |
147 | type RTMPData<T> = { data: T };
148 |
149 | export type ConnectionFailedType = NativeSyntheticEvent<RTMPData<string>>;
150 | export type ConnectionStartedType = NativeSyntheticEvent<RTMPData<string>>;
151 | export type ConnectionSuccessType = NativeSyntheticEvent<RTMPData<null>>;
152 | export type DisconnectType = NativeSyntheticEvent<RTMPData<null>>;
153 | export type NewBitrateReceivedType = NativeSyntheticEvent<RTMPData<number>>;
154 | export type StreamStateChangedType = NativeSyntheticEvent<
155 | RTMPData<StreamState>
156 | >;
157 | export type BluetoothDeviceStatusChangedType = NativeSyntheticEvent<
158 | RTMPData<BluetoothDeviceStatuses>
159 | >;
160 | export interface NativeRTMPPublisherProps {
161 | style?: ViewStyle;
162 | streamURL: string;
163 | streamName: string;
164 | onConnectionFailed?: (e: ConnectionFailedType) => void;
165 | onConnectionStarted?: (e: ConnectionStartedType) => void;
166 | onConnectionSuccess?: (e: ConnectionSuccessType) => void;
167 | onDisconnect?: (e: DisconnectType) => void;
168 | onNewBitrateReceived?: (e: NewBitrateReceivedType) => void;
169 | onStreamStateChanged?: (e: StreamStateChangedType) => void;
170 | onBluetoothDeviceStatusChanged?: (
171 | e: BluetoothDeviceStatusChangedType
172 | ) => void;
173 | }
174 | export default requireNativeComponent<NativeRTMPPublisherProps>(
175 | 'RTMPPublisher'
176 | );
177 | |
178 |
179 |
180 |
181 |
186 |
187 |
192 |
193 |
194 |
195 |
196 |
--------------------------------------------------------------------------------
/example/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
--------------------------------------------------------------------------------