Skip to content

Commit d5800dc

Browse files
committed
add native view for local video
1 parent ebe3967 commit d5800dc

File tree

13 files changed

+417
-10
lines changed

13 files changed

+417
-10
lines changed

packages/react-native-broadcast/README.md

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,51 @@ npm install react-native-broadcast
1010

1111
## Usage
1212

13-
```js
14-
import { multiply } from 'react-native-broadcast';
13+
### BroadcastVideoView Component
1514

16-
// ...
15+
Display the local video preview from the broadcast mixer:
1716

18-
const result = multiply(3, 7);
17+
```tsx
18+
import { BroadcastVideoView, multiply } from 'react-native-broadcast';
19+
import { View, StyleSheet, Button } from 'react-native';
20+
21+
function App() {
22+
const startBroadcast = async () => {
23+
// This will initialize the mixer and start the RTMP broadcast
24+
await multiply(3, 7);
25+
};
26+
27+
return (
28+
<View style={styles.container}>
29+
<BroadcastVideoView style={styles.video} />
30+
<Button title="Start Broadcast" onPress={startBroadcast} />
31+
</View>
32+
);
33+
}
34+
35+
const styles = StyleSheet.create({
36+
container: {
37+
flex: 1,
38+
},
39+
video: {
40+
flex: 1,
41+
backgroundColor: 'black',
42+
},
43+
});
1944
```
2045

46+
### API
47+
48+
#### `BroadcastVideoView`
49+
50+
A native view component that displays the local video from the broadcast mixer.
51+
52+
**Props:**
53+
54+
- `style?: ViewStyle` - Standard React Native style prop
55+
56+
The view automatically connects to the mixer when it becomes available after calling `multiply()` to start the broadcast.
57+
2158
## Contributing
2259

2360
- [Development workflow](CONTRIBUTING.md#development-workflow)

packages/react-native-broadcast/ios/Broadcast.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ import Foundation
33
import HaishinKit
44
import RTMPHaishinKit
55

6+
@objc
7+
public class BroadcastManager: NSObject {
8+
@objc public static let shared = BroadcastManager()
9+
10+
var mixer: MediaMixer?
11+
var audioSourceService: AudioSourceService?
12+
13+
private override init() {
14+
super.init()
15+
}
16+
}
17+
618
@objc
719
public class BroadcastSwift: NSObject {
820

@@ -30,6 +42,10 @@ public class BroadcastSwift: NSObject {
3042
videoMixerSettings.mode = .offscreen
3143
await mixer.setVideoMixerSettings(videoMixerSettings)
3244

45+
// Store in manager for video view access
46+
BroadcastManager.shared.mixer = mixer
47+
BroadcastManager.shared.audioSourceService = audioSourceService
48+
3349
print("[RTMP] Mixer created")
3450

3551
let front = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front)
@@ -75,3 +91,4 @@ public class BroadcastSwift: NSObject {
7591
}
7692
}
7793
}
94+
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import AVFoundation
2+
import Foundation
3+
import HaishinKit
4+
import UIKit
5+
6+
@objc(BroadcastVideoView)
7+
public class BroadcastVideoView: UIView {
8+
9+
private var hkView: MTHKView?
10+
private var isAttached = false
11+
12+
public override init(frame: CGRect) {
13+
super.init(frame: frame)
14+
setupView()
15+
}
16+
17+
required init?(coder: NSCoder) {
18+
super.init(coder: coder)
19+
setupView()
20+
}
21+
22+
private func setupView() {
23+
backgroundColor = .black
24+
25+
// Create HKView for displaying HaishinKit video
26+
let hkView = MTHKView(frame: bounds)
27+
hkView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
28+
hkView.videoGravity = .resizeAspectFill
29+
addSubview(hkView)
30+
self.hkView = hkView
31+
32+
print("[BroadcastVideoView] View initialized")
33+
34+
// Try to attach mixer if available
35+
tryAttachMixer()
36+
}
37+
38+
private func tryAttachMixer() {
39+
guard !isAttached, let mixer = BroadcastManager.shared.mixer else {
40+
// Schedule retry if mixer not ready yet
41+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
42+
self?.tryAttachMixer()
43+
}
44+
return
45+
}
46+
47+
Task { @MainActor in
48+
guard let hkView = self.hkView else { return }
49+
// await hkView.attachStream(mixer)
50+
await mixer.addOutput(hkView)
51+
self.isAttached = true
52+
print("[BroadcastVideoView] Mixer attached to view")
53+
}
54+
}
55+
56+
public override func layoutSubviews() {
57+
super.layoutSubviews()
58+
hkView?.frame = bounds
59+
}
60+
61+
deinit {
62+
print("[BroadcastVideoView] View deinitialized")
63+
}
64+
}
65+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#import <React/RCTViewManager.h>
2+
3+
@interface RCT_EXTERN_MODULE(BroadcastVideoViewManager, RCTViewManager)
4+
5+
@end
6+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Foundation
2+
import React
3+
4+
@objc(BroadcastVideoViewManager)
5+
class BroadcastVideoViewManager: RCTViewManager {
6+
7+
override func view() -> UIView! {
8+
let view = BroadcastVideoView()
9+
print("[BroadcastVideoViewManager] View created")
10+
return view
11+
}
12+
13+
override static func requiresMainQueueSetup() -> Bool {
14+
return true
15+
}
16+
}
17+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { requireNativeComponent, type ViewStyle } from 'react-native';
2+
3+
export interface BroadcastVideoViewProps {
4+
style?: ViewStyle;
5+
}
6+
7+
interface NativeBroadcastVideoViewProps {
8+
style?: ViewStyle;
9+
}
10+
11+
const NativeBroadcastVideoView =
12+
requireNativeComponent<NativeBroadcastVideoViewProps>('BroadcastVideoView');
13+
14+
export const BroadcastVideoView = ({ style }: BroadcastVideoViewProps) => {
15+
return <NativeBroadcastVideoView style={{ flex: 1, ...style }} />;
16+
};

packages/react-native-broadcast/src/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ import Broadcast from './NativeBroadcast';
33
export function multiply(a: number, b: number): Promise<number> {
44
return Broadcast.multiply(a, b);
55
}
6+
7+
export { BroadcastVideoView } from './BroadcastVideoView';
8+
export type { BroadcastVideoViewProps } from './BroadcastVideoView';

sample-apps/react-native/dogfood/App.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { NavigationHeader } from './src/components/NavigationHeader';
3434
import { GestureHandlerRootView } from 'react-native-gesture-handler';
3535
import { Alert, LogBox } from 'react-native';
3636
import { LiveStream } from './src/navigators/Livestream';
37+
import { RTMP } from './src/navigators/RTMP';
3738
import PushNotificationIOS from '@react-native-community/push-notification-ios';
3839
import {
3940
defaultTheme,
@@ -44,7 +45,6 @@ import {
4445
} from '@stream-io/video-react-native-sdk';
4546
import Toast from 'react-native-toast-message';
4647
import { appTheme } from './src/theme';
47-
import { multiply } from '@stream-io/video-react-native-broadcast';
4848

4949
// only enable warning and error logs from webrtc library
5050
Logger.enable(`${Logger.ROOT_PREFIX}:(WARN|ERROR)`);
@@ -87,10 +87,6 @@ const StackNavigator = () => {
8787
};
8888
}, []);
8989

90-
useEffect(() => {
91-
multiply(3, 7).then((r) => console.log(`OL: multiply ${r}`));
92-
}, []);
93-
9490
let mode;
9591
switch (appMode) {
9692
case 'Meeting':
@@ -129,6 +125,15 @@ const StackNavigator = () => {
129125
/>
130126
);
131127
break;
128+
case 'RTMP':
129+
mode = (
130+
<Stack.Screen
131+
name="RTMP"
132+
component={RTMP}
133+
options={{ headerShown: false }}
134+
/>
135+
);
136+
break;
132137
case 'None':
133138
mode = (
134139
<Stack.Screen

sample-apps/react-native/dogfood/src/contexts/AppContext.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import createStoreContext from './createStoreContext';
22

3-
export type AppMode = 'Meeting' | 'Call' | 'Audio-Room' | 'LiveStream' | 'None';
3+
export type AppMode =
4+
| 'Meeting'
5+
| 'Call'
6+
| 'Audio-Room'
7+
| 'LiveStream'
8+
| 'RTMP'
9+
| 'None';
410
export type ThemeMode = 'dark' | 'light';
511

612
type AppGlobalStore = {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react';
2+
import { createNativeStackNavigator } from '@react-navigation/native-stack';
3+
import { RTMPParamList } from '../../types';
4+
import { RTMPBroadcastScreen } from '../screens/RTMPBroadcastScreen';
5+
6+
const RTMPStack = createNativeStackNavigator<RTMPParamList>();
7+
8+
export const RTMP = () => {
9+
return (
10+
<RTMPStack.Navigator>
11+
<RTMPStack.Screen
12+
name="RTMPBroadcast"
13+
component={RTMPBroadcastScreen}
14+
options={{ headerShown: false }}
15+
/>
16+
</RTMPStack.Navigator>
17+
);
18+
};

0 commit comments

Comments
 (0)