├── android ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values-night │ │ │ │ │ └── colors.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ ├── drawable-hdpi │ │ │ │ │ └── splashscreen_logo.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ └── splashscreen_logo.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ └── splashscreen_logo.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ └── splashscreen_logo.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ └── splashscreen_logo.png │ │ │ │ ├── values │ │ │ │ │ ├── colors.xml │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── drawable │ │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ └── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── anonymous │ │ │ │ └── bigbluebuttontablet │ │ │ │ ├── MainApplication.kt │ │ │ │ └── MainActivity.kt │ │ └── debug │ │ │ └── AndroidManifest.xml │ ├── debug.keystore │ ├── proguard-rules.pro │ └── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── .gitignore ├── build.gradle ├── settings.gradle ├── gradle.properties ├── gradlew.bat └── gradlew ├── hooks ├── useColorScheme.ts ├── useColorScheme.web.ts └── useThemeColor.ts ├── ios ├── Assets │ └── music2.mp3 ├── Podfile.properties.json ├── bigbluebuttontablet │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── 100.png │ │ │ ├── 102.png │ │ │ ├── 108.png │ │ │ ├── 114.png │ │ │ ├── 120.png │ │ │ ├── 128.png │ │ │ ├── 144.png │ │ │ ├── 152.png │ │ │ ├── 16.png │ │ │ ├── 167.png │ │ │ ├── 172.png │ │ │ ├── 180.png │ │ │ ├── 196.png │ │ │ ├── 20.png │ │ │ ├── 216.png │ │ │ ├── 234.png │ │ │ ├── 256.png │ │ │ ├── 258.png │ │ │ ├── 29.png │ │ │ ├── 32.png │ │ │ ├── 40.png │ │ │ ├── 48.png │ │ │ ├── 50.png │ │ │ ├── 512.png │ │ │ ├── 55.png │ │ │ ├── 57.png │ │ │ ├── 58.png │ │ │ ├── 60.png │ │ │ ├── 64.png │ │ │ ├── 66.png │ │ │ ├── 72.png │ │ │ ├── 76.png │ │ │ ├── 80.png │ │ │ ├── 87.png │ │ │ ├── 88.png │ │ │ ├── 92.png │ │ │ ├── 1024.png │ │ │ └── Contents.json │ │ └── SplashScreenBackground.colorset │ │ │ └── Contents.json │ ├── bigbluebuttontablet-Bridging-Header.h │ ├── bigbluebuttontablet.entitlements │ ├── Supporting │ │ └── Expo.plist │ ├── PrivacyInfo.xcprivacy │ ├── SplashScreen.storyboard │ ├── Info.plist │ └── AppDelegate.swift ├── BigBlueButton Screen Share │ ├── BigBlueButton Screen Share-Bridging-Header.h │ ├── BigBlueButton Screen Share.entitlements │ ├── FinishBroadcastService.h │ ├── FinishBroadcastService.m │ ├── Info.plist │ └── SampleHandler.swift ├── bigbluebuttontablet.xcworkspace │ ├── xcshareddata │ │ └── WorkspaceSettings.xcsettings │ └── contents.xcworkspacedata ├── ReactExported │ ├── ReactNativeEventEmitter.m │ ├── ReactNativeScreenShareService.m │ ├── ReactNativeEventEmitter.swift │ └── ReactNativeScreenShareService.swift ├── .gitignore ├── .xcode.env ├── BigBlueButton Screen ShareSetupUI │ ├── Info.plist │ └── BroadcastSetupViewController.swift ├── ScreenSharing │ ├── WebRTC │ │ ├── IceCandidate.swift │ │ └── ScreenShareWebRTCClient.swift │ ├── ScreenSharePublisher.swift │ ├── PixelBufferSerialization.swift │ └── ScreenShareService.swift ├── Podfile ├── bigbluebuttontablet.xcodeproj │ └── xcshareddata │ │ └── xcschemes │ │ └── bigbluebuttontablet.xcscheme ├── Gemfile.lock └── InterProcessCommunication │ └── IPCFileManager.swift ├── assets ├── images │ ├── icon.png │ ├── favicon.png │ ├── react-logo.png │ ├── splash-icon.png │ ├── adaptive-icon.png │ ├── react-logo@2x.png │ ├── react-logo@3x.png │ └── partial-react-logo.png └── fonts │ └── SpaceMono-Regular.ttf ├── .vscode └── settings.json ├── components ├── ui │ ├── TabBarBackground.tsx │ ├── TabBarBackground.ios.tsx │ ├── IconSymbol.ios.tsx │ └── IconSymbol.tsx ├── ThemedView.tsx ├── HapticTab.tsx ├── ExternalLink.tsx ├── HelloWave.tsx ├── AppLogger.ts ├── Collapsible.tsx ├── ThemedText.tsx └── ParallaxScrollView.tsx ├── eslint.config.js ├── tsconfig.json ├── app ├── native-messaging │ └── emitter.tsx ├── native-components │ ├── BBBN_ScreenShareService.d.ts │ └── BBBN_ScreenShareService.tsx ├── events │ ├── onBroadcastFinished.tsx │ ├── onScreenShareSignalingStateChange.tsx │ └── onScreenShareLocalIceCandidate.tsx ├── +not-found.tsx ├── methods │ ├── stopScreenShare.tsx │ ├── createScreenShareOffer.tsx │ ├── setScreenShareRemoteSDP.tsx │ ├── initializeScreenShare.tsx │ └── addScreenShareRemoteIceCandidate.tsx ├── webview │ └── message-handler.tsx ├── _layout.tsx └── MeetingWebView.tsx ├── i18n ├── locales │ ├── en │ │ └── translation.json │ ├── pt-BR │ │ └── translation.json │ └── de │ │ └── translation.json └── index.ts ├── .gitignore ├── constants └── Colors.ts ├── fix.sh ├── app.json ├── README.md ├── package.json └── scripts └── reset-project.js /android/app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | export { useColorScheme } from 'react-native'; 2 | -------------------------------------------------------------------------------- /ios/Assets/music2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/Assets/music2.mp3 -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/debug.keystore -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/assets/images/react-logo.png -------------------------------------------------------------------------------- /assets/images/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/assets/images/splash-icon.png -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/images/react-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/assets/images/react-logo@2x.png -------------------------------------------------------------------------------- /assets/images/react-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/assets/images/react-logo@3x.png -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /assets/images/partial-react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/assets/images/partial-react-logo.png -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ios/Podfile.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo.jsEngine": "hermes", 3 | "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true", 4 | "newArchEnabled": "true" 5 | } 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/splashscreen_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/splashscreen_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": "explicit", 4 | "source.organizeImports": "explicit", 5 | "source.sortMembers": "explicit" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/102.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/102.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/108.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/172.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/196.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/216.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/234.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/234.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/258.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/258.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/48.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/55.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/66.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/88.png -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/92.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/92.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbluebutton/bigbluebutton-mobile/HEAD/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /components/ui/TabBarBackground.tsx: -------------------------------------------------------------------------------- 1 | // This is a shim for web and Android where the tab bar is generally opaque. 2 | export default undefined; 3 | 4 | export function useBottomTabOverflow() { 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Android/IntelliJ 6 | # 7 | build/ 8 | .idea 9 | .gradle 10 | local.properties 11 | *.iml 12 | *.hprof 13 | .cxx/ 14 | 15 | # Bundle artifacts 16 | *.jsbundle 17 | -------------------------------------------------------------------------------- /ios/BigBlueButton Screen Share/BigBlueButton Screen Share-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "FinishBroadcastService.h" 6 | -------------------------------------------------------------------------------- /ios/bigbluebuttontablet.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff 3 | #ffffff 4 | #023c69 5 | #ffffff 6 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // https://docs.expo.dev/guides/using-eslint/ 2 | const { defineConfig } = require('eslint/config'); 3 | const expoConfig = require('eslint-config-expo/flat'); 4 | 5 | module.exports = defineConfig([ 6 | expoConfig, 7 | { 8 | ignores: ['dist/*'], 9 | }, 10 | ]); 11 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ios/bigbluebuttontablet.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/bigbluebuttontablet-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // bigbluebuttontablet-Bridging-Header.h 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | #import 5 | #import 6 | #import 7 | #import 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": [ 7 | "./*" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "**/*.tsx", 14 | ".expo/types/**/*.ts", 15 | "expo-env.d.ts", 16 | "i18n" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /app/native-messaging/emitter.tsx: -------------------------------------------------------------------------------- 1 | import * as reactNative from 'react-native'; 2 | // ... 3 | const emitter: reactNative.EventEmitter = 4 | reactNative.Platform.OS === 'ios' 5 | ? new reactNative.NativeEventEmitter( 6 | reactNative.NativeModules.ReactNativeEventEmitter 7 | ) 8 | : reactNative.DeviceEventEmitter; 9 | 10 | export default emitter; 11 | -------------------------------------------------------------------------------- /app/native-components/BBBN_ScreenShareService.d.ts: -------------------------------------------------------------------------------- 1 | export function initializeScreenShare(): void; 2 | export function createScreenShareOffer(stunTurnJson: string): void; 3 | export function setScreenShareRemoteSDP(remoteSDP: string): void; 4 | export function addScreenShareRemoteIceCandidate(remoteCandidateJson: string): void; 5 | export function stopScreenShareBroadcastExtension(): void; -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | bigbluebutton-tablet 3 | automatic 4 | contain 5 | false 6 | -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/bigbluebuttontablet.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.org.bigbluebutton.tablet 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/ReactExported/ReactNativeEventEmitter.m: -------------------------------------------------------------------------------- 1 | // 2 | // ReactNativeEventEmitter.m 3 | // 4 | // Created by Tiago Daniel Jacobs on 11/03/22. 5 | // 6 | 7 | #import 8 | #import 9 | #import 10 | 11 | @interface RCT_EXTERN_MODULE(ReactNativeEventEmitter, RCTEventEmitter) 12 | 13 | RCT_EXTERN_METHOD(supportedEvents) 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /ios/BigBlueButton Screen Share/BigBlueButton Screen Share.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.org.bigbluebutton.tablet 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Supporting/Expo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EXUpdatesCheckOnLaunch 6 | ALWAYS 7 | EXUpdatesEnabled 8 | 9 | EXUpdatesLaunchWaitMs 10 | 0 11 | 12 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | .xcode.env.local 25 | 26 | # Bundle artifacts 27 | *.jsbundle 28 | 29 | # CocoaPods 30 | /Pods/ 31 | -------------------------------------------------------------------------------- /ios/BigBlueButton Screen Share/FinishBroadcastService.h: -------------------------------------------------------------------------------- 1 | // 2 | // SampleHandler.h 3 | // BigBlueButton Broadcast 4 | // 5 | // Created by Gustavo Emanuel Farias Rosa on 09/05/22. 6 | // 7 | 8 | #ifndef SampleHandler_h 9 | #define SampleHandler_h 10 | 11 | #import 12 | #import 13 | 14 | void finishBroadcastGracefully(RPBroadcastSampleHandler * _Nonnull broadcastSampleHandler); 15 | 16 | #endif /* SampleHandler_h */ 17 | -------------------------------------------------------------------------------- /i18n/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "home": { 3 | "title": "BigBlueButton", 4 | "subtitle": "Join meetings with extra features—like screen sharing", 5 | "description": "You can join a meeting directly, or transfer one from another device by scanning a QR code.", 6 | "inputLabel": "Paste your meeting link below:", 7 | "inputPlaceholder": "e.g. https://your-meeting-url", 8 | "joinButton": "Join Meeting", 9 | "screenShareButton": "Start Screen Share" 10 | } 11 | } -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Images.xcassets/SplashScreenBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": [ 3 | { 4 | "color": { 5 | "components": { 6 | "alpha": "1.000", 7 | "blue": "1.00000000000000", 8 | "green": "1.00000000000000", 9 | "red": "1.00000000000000" 10 | }, 11 | "color-space": "srgb" 12 | }, 13 | "idiom": "universal" 14 | } 15 | ], 16 | "info": { 17 | "version": 1, 18 | "author": "expo" 19 | } 20 | } -------------------------------------------------------------------------------- /ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /i18n/locales/pt-BR/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "home": { 3 | "title": "BigBlueButton", 4 | "subtitle": "Participe de reuniões com recursos extras—como compartilhamento de tela", 5 | "description": "Você pode entrar em uma reunião diretamente ou transferi-la de outro dispositivo escaneando um QR code.", 6 | "inputLabel": "Cole o link da reunião abaixo:", 7 | "inputPlaceholder": "ex: https://seu-link-de-reuniao", 8 | "joinButton": "Entrar na Reunião", 9 | "screenShareButton": "Iniciar Compartilhamento de Tela" 10 | } 11 | } -------------------------------------------------------------------------------- /components/ThemedView.tsx: -------------------------------------------------------------------------------- 1 | import { View, type ViewProps } from 'react-native'; 2 | 3 | import { useThemeColor } from '@/hooks/useThemeColor'; 4 | 5 | export type ThemedViewProps = ViewProps & { 6 | lightColor?: string; 7 | darkColor?: string; 8 | }; 9 | 10 | export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) { 11 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); 12 | 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /i18n/locales/de/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "home": { 3 | "title": "BigBlueButton", 4 | "subtitle": "Nehmen Sie an Meetings mit zusätzlichen Funktionen teil – wie Bildschirmfreigabe", 5 | "description": "Sie können direkt an einem Meeting teilnehmen oder eines von einem anderen Gerät per QR-Code übertragen.", 6 | "inputLabel": "Fügen Sie unten Ihren Meeting-Link ein:", 7 | "inputPlaceholder": "z.B. https://ihr-meeting-link", 8 | "joinButton": "Meeting beitreten", 9 | "screenShareButton": "Bildschirmübertragung starten" 10 | } 11 | } -------------------------------------------------------------------------------- /ios/BigBlueButton Screen Share/FinishBroadcastService.m: -------------------------------------------------------------------------------- 1 | // 2 | // SampleHandler.m 3 | // BigBlueButton Broadcast 4 | // 5 | // Created by Gustavo Emanuel Farias Rosa on 09/05/22. 6 | // 7 | 8 | #import 9 | 10 | #import "FinishBroadcastService.h" 11 | 12 | void finishBroadcastGracefully(RPBroadcastSampleHandler * _Nonnull broadcastSampleHandler) { 13 | #pragma clang diagnostic push 14 | #pragma clang diagnostic ignored "-Wnonnull" 15 | [broadcastSampleHandler finishBroadcastWithError:nil]; 16 | #pragma clang diagnostic pop 17 | } 18 | -------------------------------------------------------------------------------- /hooks/useColorScheme.web.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useColorScheme as useRNColorScheme } from 'react-native'; 3 | 4 | /** 5 | * To support static rendering, this value needs to be re-calculated on the client side for web 6 | */ 7 | export function useColorScheme() { 8 | const [hasHydrated, setHasHydrated] = useState(false); 9 | 10 | useEffect(() => { 11 | setHasHydrated(true); 12 | }, []); 13 | 14 | const colorScheme = useRNColorScheme(); 15 | 16 | if (hasHydrated) { 17 | return colorScheme; 18 | } 19 | 20 | return 'light'; 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | expo-env.d.ts 11 | 12 | # Native 13 | .kotlin/ 14 | *.orig.* 15 | *.jks 16 | *.p8 17 | *.p12 18 | *.key 19 | *.mobileprovision 20 | 21 | # Metro 22 | .metro-health-check* 23 | 24 | # debug 25 | npm-debug.* 26 | yarn-debug.* 27 | yarn-error.* 28 | 29 | # macOS 30 | .DS_Store 31 | *.pem 32 | 33 | # local env files 34 | .env*.local 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | 39 | app-example 40 | -------------------------------------------------------------------------------- /ios/BigBlueButton Screen Share/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.broadcast-services-upload 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SampleHandler 11 | RPBroadcastProcessMode 12 | RPBroadcastProcessModeSampleBuffer 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # react-native-reanimated 11 | -keep class com.swmansion.reanimated.** { *; } 12 | -keep class com.facebook.react.turbomodule.** { *; } 13 | 14 | # Add any project specific keep options here: 15 | -------------------------------------------------------------------------------- /hooks/useThemeColor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about light and dark modes: 3 | * https://docs.expo.dev/guides/color-schemes/ 4 | */ 5 | 6 | import { Colors } from '@/constants/Colors'; 7 | import { useColorScheme } from '@/hooks/useColorScheme'; 8 | 9 | export function useThemeColor( 10 | props: { light?: string; dark?: string }, 11 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark 12 | ) { 13 | const theme = useColorScheme() ?? 'light'; 14 | const colorFromProps = props[theme]; 15 | 16 | if (colorFromProps) { 17 | return colorFromProps; 18 | } else { 19 | return Colors[theme][colorName]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /components/HapticTab.tsx: -------------------------------------------------------------------------------- 1 | import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs'; 2 | import { PlatformPressable } from '@react-navigation/elements'; 3 | import * as Haptics from 'expo-haptics'; 4 | 5 | export function HapticTab(props: BottomTabBarButtonProps) { 6 | return ( 7 | { 10 | if (process.env.EXPO_OS === 'ios') { 11 | // Add a soft haptic feedback when pressing down on the tabs. 12 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); 13 | } 14 | props.onPressIn?.(ev); 15 | }} 16 | /> 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /components/ui/TabBarBackground.ios.tsx: -------------------------------------------------------------------------------- 1 | import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; 2 | import { BlurView } from 'expo-blur'; 3 | import { StyleSheet } from 'react-native'; 4 | 5 | export default function BlurTabBarBackground() { 6 | return ( 7 | 14 | ); 15 | } 16 | 17 | export function useBottomTabOverflow() { 18 | return useBottomTabBarHeight(); 19 | } 20 | -------------------------------------------------------------------------------- /app/events/onBroadcastFinished.tsx: -------------------------------------------------------------------------------- 1 | import type { MutableRefObject } from 'react'; 2 | import type { EmitterSubscription } from 'react-native'; 3 | import nativeEmitter from '../native-messaging/emitter'; 4 | 5 | export function setupListener( 6 | _webViewRef: MutableRefObject 7 | ): EmitterSubscription { 8 | // Resolve promise when SDP offer is available 9 | return nativeEmitter.addListener('onBroadcastFinished', () => { 10 | console.log(`Broadcast finished`); 11 | _webViewRef.current.injectJavaScript( 12 | `window.bbbMobileScreenShareBroadcastFinishedCallback && window.bbbMobileScreenShareBroadcastFinishedCallback();` 13 | ); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | -------------------------------------------------------------------------------- /ios/ReactExported/ReactNativeScreenShareService.m: -------------------------------------------------------------------------------- 1 | // 2 | // ReactNativeScreenShareService.m 3 | // 4 | // Created by Tiago Daniel Jacobs on 11/03/22. 5 | // 6 | 7 | #import 8 | 9 | #import "React/RCTBridgeModule.h" 10 | @interface RCT_EXTERN_REMAP_MODULE(BBBN_ScreenShareService, ReactNativeScreenShareService, NSObject) 11 | 12 | RCT_EXTERN_METHOD(stopScreenShareBroadcastExtension) 13 | RCT_EXTERN_METHOD(initializeScreenShare) 14 | RCT_EXTERN_METHOD(createScreenShareOffer: (NSString *)stunTurnJson) 15 | RCT_EXTERN_METHOD(setScreenShareRemoteSDP: (NSString *)remoteSDP) 16 | RCT_EXTERN_METHOD(addScreenShareRemoteIceCandidate: (NSString *)remoteCandidate) 17 | 18 | + (BOOL)requiresMainQueueSetup { return NO; } 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /ios/BigBlueButton Screen ShareSetupUI/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionAttributes 8 | 9 | NSExtensionActivationRule 10 | 11 | NSExtensionActivationSupportsReplayKitStreaming 12 | 13 | 14 | 15 | NSExtensionPointIdentifier 16 | com.apple.broadcast-services-setupui 17 | NSExtensionPrincipalClass 18 | $(PRODUCT_MODULE_NAME).BroadcastSetupViewController 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /components/ui/IconSymbol.ios.tsx: -------------------------------------------------------------------------------- 1 | import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols'; 2 | import { StyleProp, ViewStyle } from 'react-native'; 3 | 4 | export function IconSymbol({ 5 | name, 6 | size = 24, 7 | color, 8 | style, 9 | weight = 'regular', 10 | }: { 11 | name: SymbolViewProps['name']; 12 | size?: number; 13 | color: string; 14 | style?: StyleProp; 15 | weight?: SymbolWeight; 16 | }) { 17 | return ( 18 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/events/onScreenShareSignalingStateChange.tsx: -------------------------------------------------------------------------------- 1 | import type { MutableRefObject } from 'react'; 2 | import type { EmitterSubscription } from 'react-native'; 3 | import nativeEmitter from '../native-messaging/emitter'; 4 | 5 | export function setupListener( 6 | _webViewRef: MutableRefObject 7 | ): EmitterSubscription { 8 | // Resolve promise when SDP offer is available 9 | return nativeEmitter.addListener( 10 | 'onScreenShareSignalingStateChange', 11 | (newState) => { 12 | console.log(`Temos um novo state: ${newState}`); 13 | _webViewRef.current.injectJavaScript( 14 | `window.bbbMobileScreenShareSignalingStateChangeCallback && window.bbbMobileScreenShareSignalingStateChangeCallback(${JSON.stringify( 15 | newState 16 | )});` 17 | ); 18 | } 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | import { Href, Link } from 'expo-router'; 2 | import { openBrowserAsync } from 'expo-web-browser'; 3 | import { type ComponentProps } from 'react'; 4 | import { Platform } from 'react-native'; 5 | 6 | type Props = Omit, 'href'> & { href: Href & string }; 7 | 8 | export function ExternalLink({ href, ...rest }: Props) { 9 | return ( 10 | { 15 | if (Platform.OS !== 'web') { 16 | // Prevent the default behavior of linking to the default browser on native. 17 | event.preventDefault(); 18 | // Open the link in an in-app browser. 19 | await openBrowserAsync(href); 20 | } 21 | }} 22 | /> 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /constants/Colors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Below are the colors that are used in the app. The colors are defined in the light and dark mode. 3 | * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. 4 | */ 5 | 6 | const tintColorLight = '#0a7ea4'; 7 | const tintColorDark = '#fff'; 8 | 9 | export const Colors = { 10 | light: { 11 | text: '#11181C', 12 | background: '#fff', 13 | tint: tintColorLight, 14 | icon: '#687076', 15 | tabIconDefault: '#687076', 16 | tabIconSelected: tintColorLight, 17 | }, 18 | dark: { 19 | text: '#ECEDEE', 20 | background: '#151718', 21 | tint: tintColorDark, 22 | icon: '#9BA1A6', 23 | tabIconDefault: '#9BA1A6', 24 | tabIconSelected: tintColorDark, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /ios/ScreenSharing/WebRTC/IceCandidate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IceCandidate.swift 3 | // WebRTC-Demo 4 | // 5 | // Created by Stasel on 20/02/2019. 6 | // Copyright © 2019 Stasel. All rights reserved. 7 | // 8 | import Foundation 9 | import WebRTC 10 | 11 | /// This struct is a swift wrapper over `RTCIceCandidate` for easy encode and decode 12 | public struct IceCandidate: Codable { 13 | let candidate: String 14 | let sdpMLineIndex: Int32 15 | let sdpMid: String? 16 | 17 | public init(from iceCandidate: RTCIceCandidate) { 18 | self.sdpMLineIndex = iceCandidate.sdpMLineIndex 19 | self.sdpMid = iceCandidate.sdpMid 20 | self.candidate = iceCandidate.sdp 21 | } 22 | 23 | var rtcIceCandidate: RTCIceCandidate { 24 | return RTCIceCandidate(sdp: self.candidate, sdpMLineIndex: self.sdpMLineIndex, sdpMid: self.sdpMid) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/native-components/BBBN_ScreenShareService.tsx: -------------------------------------------------------------------------------- 1 | import { NativeModules } from 'react-native'; 2 | 3 | const ScreenShareService = NativeModules.BBBN_ScreenShareService; 4 | 5 | console.log('ScreenShareService', ScreenShareService); 6 | 7 | export function initializeScreenShare() { 8 | ScreenShareService.initializeScreenShare(); 9 | } 10 | 11 | export function createScreenShareOffer(stunTurnJson: String) { 12 | ScreenShareService.createScreenShareOffer(stunTurnJson); 13 | } 14 | 15 | export function setScreenShareRemoteSDP(remoteSDP: string) { 16 | ScreenShareService.setScreenShareRemoteSDP(remoteSDP); 17 | } 18 | 19 | export function addScreenShareRemoteIceCandidate(remoteCandidateJson: string) { 20 | ScreenShareService.addScreenShareRemoteIceCandidate(remoteCandidateJson); 21 | } 22 | 23 | export function stopScreenShareBroadcastExtension() { 24 | ScreenShareService.stopScreenShareBroadcastExtension(); 25 | } 26 | -------------------------------------------------------------------------------- /app/+not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Link, Stack } from 'expo-router'; 2 | import { StyleSheet } from 'react-native'; 3 | 4 | import { ThemedText } from '@/components/ThemedText'; 5 | import { ThemedView } from '@/components/ThemedView'; 6 | 7 | export default function NotFoundScreen() { 8 | return ( 9 | <> 10 | 11 | 12 | This screen does not exist. 13 | 14 | Go to home screen! 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | const styles = StyleSheet.create({ 22 | container: { 23 | flex: 1, 24 | alignItems: 'center', 25 | justifyContent: 'center', 26 | padding: 20, 27 | }, 28 | link: { 29 | marginTop: 15, 30 | paddingVertical: 15, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /app/events/onScreenShareLocalIceCandidate.tsx: -------------------------------------------------------------------------------- 1 | import type { MutableRefObject } from 'react'; 2 | import type { EmitterSubscription } from 'react-native'; 3 | import nativeEmitter from '../native-messaging/emitter'; 4 | 5 | export function setupListener( 6 | _webViewRef: MutableRefObject 7 | ): EmitterSubscription { 8 | // Resolve promise when SDP offer is available 9 | return nativeEmitter.addListener( 10 | 'onScreenShareLocalIceCandidate', 11 | (jsonEncodedIceCandidate) => { 12 | let iceCandidate = JSON.parse(jsonEncodedIceCandidate); 13 | if (typeof iceCandidate === 'string') { 14 | iceCandidate = JSON.parse(iceCandidate); 15 | } 16 | const event = { candidate: iceCandidate }; 17 | _webViewRef.current.injectJavaScript( 18 | `window.bbbMobileScreenShareIceCandidateCallback && window.bbbMobileScreenShareIceCandidateCallback(${JSON.stringify( 19 | event 20 | )});` 21 | ); 22 | } 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /fix.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Run this from the root of the repository. 3 | 4 | git filter-branch --env-filter ' 5 | # ---------- hard-coded old identity ----------------------------------- 6 | OLD_EMAIL="tiago@MacBook-Pro-de-Tiago.local" 7 | # ---------- hard-coded new identity ----------------------------------- 8 | NEW_NAME="Tiago Daniel Jacobs" 9 | NEW_EMAIL="tiago.jacobs@gmail.com" 10 | # ---------------------------------------------------------------------- 11 | 12 | # If the author is wrong, rewrite it 13 | if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]; then 14 | export GIT_AUTHOR_NAME="$NEW_NAME" 15 | export GIT_AUTHOR_EMAIL="$NEW_EMAIL" 16 | fi 17 | 18 | # If the committer is wrong, rewrite it 19 | if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]; then 20 | export GIT_COMMITTER_NAME="$NEW_NAME" 21 | export GIT_COMMITTER_EMAIL="$NEW_EMAIL" 22 | fi 23 | ' --tag-name-filter cat -- --branches --tags 24 | 25 | echo "" 26 | echo "✔️ All done. If this repo is on a remote you own, force-push now:" 27 | echo " git push --force --tags" 28 | 29 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath('com.android.tools.build:gradle') 10 | classpath('com.facebook.react:react-native-gradle-plugin') 11 | classpath('org.jetbrains.kotlin:kotlin-gradle-plugin') 12 | } 13 | } 14 | 15 | def reactNativeAndroidDir = new File( 16 | providers.exec { 17 | workingDir(rootDir) 18 | commandLine("node", "--print", "require.resolve('react-native/package.json')") 19 | }.standardOutput.asText.get().trim(), 20 | "../android" 21 | ) 22 | 23 | allprojects { 24 | repositories { 25 | maven { 26 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 27 | url(reactNativeAndroidDir) 28 | } 29 | 30 | google() 31 | mavenCentral() 32 | maven { url 'https://www.jitpack.io' } 33 | } 34 | } 35 | 36 | apply plugin: "expo-root-project" 37 | apply plugin: "com.facebook.react.rootproject" 38 | -------------------------------------------------------------------------------- /components/HelloWave.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | import Animated, { 4 | useAnimatedStyle, 5 | useSharedValue, 6 | withRepeat, 7 | withSequence, 8 | withTiming, 9 | } from 'react-native-reanimated'; 10 | 11 | import { ThemedText } from '@/components/ThemedText'; 12 | 13 | export function HelloWave() { 14 | const rotationAnimation = useSharedValue(0); 15 | 16 | useEffect(() => { 17 | rotationAnimation.value = withRepeat( 18 | withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })), 19 | 4 // Run the animation 4 times 20 | ); 21 | }, [rotationAnimation]); 22 | 23 | const animatedStyle = useAnimatedStyle(() => ({ 24 | transform: [{ rotate: `${rotationAnimation.value}deg` }], 25 | })); 26 | 27 | return ( 28 | 29 | 👋 30 | 31 | ); 32 | } 33 | 34 | const styles = StyleSheet.create({ 35 | text: { 36 | fontSize: 28, 37 | lineHeight: 32, 38 | marginTop: -6, 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /i18n/index.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | import * as Localization from 'expo-localization'; 3 | import i18n from 'i18next'; 4 | import { initReactI18next } from 'react-i18next'; 5 | import translationDe from './locales/de/translation.json'; 6 | import translationEn from './locales/en/translation.json'; 7 | import translationPt from './locales/pt-BR/translation.json'; 8 | 9 | const resources = { 10 | en: { translation: translationEn }, 11 | 'pt-BR': { translation: translationPt }, 12 | de: { translation: translationDe }, 13 | }; 14 | 15 | const LANGUAGE_KEY = 'language'; 16 | 17 | const initI18n = async () => { 18 | let savedLanguage = await AsyncStorage.getItem(LANGUAGE_KEY); 19 | if (!savedLanguage) { 20 | savedLanguage = Localization.getLocales()[0]?.languageTag || 'en'; 21 | } 22 | const lng = Object.keys(resources).includes(savedLanguage) ? savedLanguage : 'en'; 23 | i18n.use(initReactI18next).init({ 24 | resources, 25 | lng, 26 | fallbackLng: 'en', 27 | interpolation: { escapeValue: false }, 28 | }); 29 | }; 30 | 31 | initI18n(); 32 | 33 | export default i18n; -------------------------------------------------------------------------------- /app/methods/stopScreenShare.tsx: -------------------------------------------------------------------------------- 1 | import { stopScreenShareBroadcastExtension as nativeStopScreenShare } from '../native-components/BBBN_ScreenShareService'; 2 | import nativeEmitter from '../native-messaging/emitter'; 3 | 4 | // Reference to the resolver of last call 5 | let resolve = (a: String | null) => { 6 | console.log( 7 | `default resolve function called, this should never happen: ${a}` 8 | ); 9 | }; 10 | 11 | // Resolve promise when broadcast is started (this event means that user confirmed the screenshare) 12 | nativeEmitter.addListener('onBroadcastFinished', () => { 13 | resolve(null); 14 | }); 15 | 16 | // Entry point of this method 17 | function stopScreenShare(instanceId: Number) { 18 | return new Promise((res, rej) => { 19 | // store the resolver for later call (when event is received) 20 | resolve = res; 21 | 22 | try { 23 | // call native swift method that triggers the broadcast popup 24 | console.log(`[${instanceId}] - >stopScreenShare`); 25 | nativeStopScreenShare(); 26 | } catch (e) { 27 | rej(`Call to stopScreenShare failed zzy`); 28 | } 29 | }); 30 | } 31 | 32 | export default stopScreenShare; 33 | -------------------------------------------------------------------------------- /app/methods/createScreenShareOffer.tsx: -------------------------------------------------------------------------------- 1 | import { createScreenShareOffer as nativeCreateScreenShareOffer } from '../native-components/BBBN_ScreenShareService'; 2 | import nativeEmitter from '../native-messaging/emitter'; 3 | 4 | // Reference to the resolver of last call 5 | let resolve = (a: String) => { 6 | console.log( 7 | `default resolve function called, this should never happen: ${a}` 8 | ); 9 | }; 10 | 11 | // Resolve promise when SDP offer is available 12 | nativeEmitter.addListener('onScreenShareOfferCreated', (sdp) => { 13 | resolve(sdp); 14 | }); 15 | 16 | // Entry point of this method 17 | function createScreenShareOffer(instanceId: Number, stunTurnJson: String) { 18 | return new Promise((res, rej) => { 19 | // store the resolver for later call (when event is received) 20 | resolve = res; 21 | 22 | try { 23 | console.log( 24 | `[${instanceId}] - >nativeCreateScreenShareOffer (${stunTurnJson})` 25 | ); 26 | // call native swift method that triggers the broadcast popup 27 | nativeCreateScreenShareOffer(stunTurnJson); 28 | } catch (e) { 29 | rej(`Call to nativeCreateScreenShareOffer failed`); 30 | } 31 | }); 32 | } 33 | 34 | export default createScreenShareOffer; 35 | -------------------------------------------------------------------------------- /app/methods/setScreenShareRemoteSDP.tsx: -------------------------------------------------------------------------------- 1 | import { setScreenShareRemoteSDP as nativeSetScreenShareRemoteSDP } from '../native-components/BBBN_ScreenShareService'; 2 | import nativeEmitter from '../native-messaging/emitter'; 3 | 4 | // Reference to the resolver of last call 5 | let resolve = (value: unknown) => { 6 | console.log( 7 | `default resolve function called, this should never happen: ${value}` 8 | ); 9 | }; 10 | 11 | // Resolve promise when SDP offer is available 12 | nativeEmitter.addListener('onSetScreenShareRemoteSDPCompleted', () => { 13 | resolve(undefined); 14 | }); 15 | 16 | // Entry point of this method 17 | function setScreenShareRemoteSDP(instanceId: Number, remoteSdp: string) { 18 | return new Promise((res, rej) => { 19 | // store the resolver for later call (when event is received) 20 | resolve = res; 21 | 22 | try { 23 | console.log( 24 | `[${instanceId}] - >nativeSetScreenShareRemoteSDP ${remoteSdp}` 25 | ); 26 | // call native swift method that triggers the broadcast popup 27 | nativeSetScreenShareRemoteSDP(remoteSdp); 28 | } catch (e) { 29 | rej(`Call to nativeSetScreenShareRemoteSDP failed`); 30 | } 31 | }); 32 | } 33 | 34 | export default setScreenShareRemoteSDP; 35 | -------------------------------------------------------------------------------- /components/AppLogger.ts: -------------------------------------------------------------------------------- 1 | // --- AppLogger Singleton --- 2 | export class AppLogger { 3 | static instance: AppLogger; 4 | private listeners: ((logs: string[]) => void)[] = []; 5 | private logs: string[] = []; 6 | 7 | private constructor() {} 8 | 9 | static getInstance() { 10 | if (!AppLogger.instance) { 11 | AppLogger.instance = new AppLogger(); 12 | } 13 | return AppLogger.instance; 14 | } 15 | 16 | info(msg: string) { 17 | this.addLog('INFO', msg); 18 | } 19 | 20 | debug(msg: string) { 21 | this.addLog('DEBUG', msg); 22 | } 23 | 24 | private addLog(level: string, msg: string) { 25 | const entry = `[${level}] ${new Date().toISOString()} ${msg}`; 26 | this.logs.push(entry); 27 | console.log(entry); 28 | setTimeout(() => { 29 | this.listeners.forEach((cb) => cb([...this.logs])); 30 | }, 0); 31 | } 32 | 33 | getLogs() { 34 | return this.logs; 35 | } 36 | 37 | subscribe(cb: (logs: string[]) => void) { 38 | this.listeners.push(cb); 39 | return () => { 40 | this.listeners = this.listeners.filter((l) => l !== cb); 41 | }; 42 | } 43 | 44 | clear() { 45 | this.logs = []; 46 | setTimeout(() => { 47 | this.listeners.forEach((cb) => cb([...this.logs])); 48 | }, 0); 49 | } 50 | } -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "bigbluebutton-tablet", 4 | "slug": "bigbluebutton-tablet", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "bigbluebuttontablet", 9 | "userInterfaceStyle": "automatic", 10 | "newArchEnabled": true, 11 | "ios": { 12 | "supportsTablet": true, 13 | "bundleIdentifier": "com.anonymous.bigbluebuttontablet" 14 | }, 15 | "android": { 16 | "adaptiveIcon": { 17 | "foregroundImage": "./assets/images/adaptive-icon.png", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "edgeToEdgeEnabled": true, 21 | "package": "com.anonymous.bigbluebuttontablet" 22 | }, 23 | "web": { 24 | "bundler": "metro", 25 | "output": "static", 26 | "favicon": "./assets/images/favicon.png" 27 | }, 28 | "plugins": [ 29 | "expo-router", 30 | [ 31 | "expo-splash-screen", 32 | { 33 | "image": "./assets/images/splash-icon.png", 34 | "imageWidth": 200, 35 | "resizeMode": "contain", 36 | "backgroundColor": "#ffffff" 37 | } 38 | ], 39 | "expo-localization" 40 | ], 41 | "experiments": { 42 | "typedRoutes": true 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/methods/initializeScreenShare.tsx: -------------------------------------------------------------------------------- 1 | import { initializeScreenShare as nativeInitializeScreenShare } from '../native-components/BBBN_ScreenShareService'; 2 | import nativeEmitter from '../native-messaging/emitter'; 3 | 4 | // Reference to the resolver of last call 5 | let resolve = (a: String | null) => { 6 | console.log( 7 | `default resolve function called, this should never happen: ${a}` 8 | ); 9 | }; 10 | 11 | // Log a message when broadcast is requested 12 | nativeEmitter.addListener('onBroadcastRequested', () => { 13 | console.log(`Broadcast requested`); 14 | }); 15 | 16 | // Resolve promise when broadcast is started (this event means that user confirmed the screenshare) 17 | nativeEmitter.addListener('onBroadcastStarted', () => { 18 | resolve(null); 19 | }); 20 | 21 | // Entry point of this method 22 | function initializeScreenShare(instanceId: Number) { 23 | return new Promise((res, rej) => { 24 | // store the resolver for later call (when event is received) 25 | resolve = res; 26 | 27 | try { 28 | // call native swift method that triggers the broadcast popup 29 | console.log(`[${instanceId}] - >nativeInitializeScreenShare`); 30 | nativeInitializeScreenShare(); 31 | } catch (e) { 32 | rej(`Call to nativeInitializeScreenShare failed zzy`); 33 | } 34 | }); 35 | } 36 | 37 | export default initializeScreenShare; 38 | -------------------------------------------------------------------------------- /app/methods/addScreenShareRemoteIceCandidate.tsx: -------------------------------------------------------------------------------- 1 | import { addScreenShareRemoteIceCandidate as nativeAddScreenShareRemoteIceCandidate } from '../native-components/BBBN_ScreenShareService'; 2 | import nativeEmitter from '../native-messaging/emitter'; 3 | 4 | // Reference to the resolver of last call 5 | let resolve = (value: unknown) => { 6 | console.log( 7 | `default resolve function called, this should never happen: ${value}` 8 | ); 9 | }; 10 | 11 | // Resolve promise when SDP offer is available 12 | nativeEmitter.addListener('onAddScreenShareRemoteIceCandidateCompleted', () => { 13 | resolve(undefined); 14 | }); 15 | 16 | // Entry point of this method 17 | function addScreenShareRemoteIceCandidate( 18 | instanceId: Number, 19 | remoteCandidateJson: string 20 | ) { 21 | return new Promise((res, rej) => { 22 | // store the resolver for later call (when event is received) 23 | resolve = res; 24 | 25 | try { 26 | console.log( 27 | `[${instanceId}] - >nativeAddScreenShareRemoteIceCandidate ${remoteCandidateJson}` 28 | ); 29 | // call native swift method that triggers the broadcast popup 30 | nativeAddScreenShareRemoteIceCandidate(remoteCandidateJson); 31 | } catch (e) { 32 | rej(`Call to nativeAddScreenShareRemoteIceCandidate failed`); 33 | } 34 | }); 35 | } 36 | 37 | export default addScreenShareRemoteIceCandidate; 38 | -------------------------------------------------------------------------------- /ios/BigBlueButton Screen ShareSetupUI/BroadcastSetupViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BroadcastSetupViewController.swift 3 | // BigBlueButton Screen ShareSetupUI 4 | // 5 | // Created by Tiago Daniel Jacobs on 22/05/25. 6 | // 7 | 8 | import ReplayKit 9 | 10 | class BroadcastSetupViewController: UIViewController { 11 | 12 | // Call this method when the user has finished interacting with the view controller and a broadcast stream can start 13 | func userDidFinishSetup() { 14 | // URL of the resource where broadcast can be viewed that will be returned to the application 15 | let broadcastURL = URL(string:"http://apple.com/broadcast/streamID") 16 | 17 | // Dictionary with setup information that will be provided to broadcast extension when broadcast is started 18 | let setupInfo: [String : NSCoding & NSObjectProtocol] = ["broadcastName": "example" as NSCoding & NSObjectProtocol] 19 | 20 | // Tell ReplayKit that the extension is finished setting up and can begin broadcasting 21 | self.extensionContext?.completeRequest(withBroadcast: broadcastURL!, setupInfo: setupInfo) 22 | } 23 | 24 | func userDidCancelSetup() { 25 | let error = NSError(domain: "YouAppDomain", code: -1, userInfo: nil) 26 | // Tell ReplayKit that the extension was cancelled by the user 27 | self.extensionContext?.cancelRequest(withError: error) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def reactNativeGradlePlugin = new File( 3 | providers.exec { 4 | workingDir(rootDir) 5 | commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })") 6 | }.standardOutput.asText.get().trim() 7 | ).getParentFile().absolutePath 8 | includeBuild(reactNativeGradlePlugin) 9 | 10 | def expoPluginsPath = new File( 11 | providers.exec { 12 | workingDir(rootDir) 13 | commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })") 14 | }.standardOutput.asText.get().trim(), 15 | "../android/expo-gradle-plugin" 16 | ).absolutePath 17 | includeBuild(expoPluginsPath) 18 | } 19 | 20 | plugins { 21 | id("com.facebook.react.settings") 22 | id("expo-autolinking-settings") 23 | } 24 | 25 | extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> 26 | if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') { 27 | ex.autolinkLibrariesFromCommand() 28 | } else { 29 | ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand) 30 | } 31 | } 32 | expoAutolinking.useExpoModules() 33 | 34 | rootProject.name = 'bigbluebutton-tablet' 35 | 36 | expoAutolinking.useExpoVersionCatalog() 37 | 38 | include ':app' 39 | includeBuild(expoAutolinking.reactNativeGradlePlugin) 40 | -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryUserDefaults 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | CA92.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategoryFileTimestamp 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | 0A2A.1 21 | 3B52.1 22 | C617.1 23 | 24 | 25 | 26 | NSPrivacyAccessedAPIType 27 | NSPrivacyAccessedAPICategoryDiskSpace 28 | NSPrivacyAccessedAPITypeReasons 29 | 30 | E174.1 31 | 85F4.1 32 | 33 | 34 | 35 | NSPrivacyAccessedAPIType 36 | NSPrivacyAccessedAPICategorySystemBootTime 37 | NSPrivacyAccessedAPITypeReasons 38 | 39 | 35F9.1 40 | 41 | 42 | 43 | NSPrivacyCollectedDataTypes 44 | 45 | NSPrivacyTracking 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /components/Collapsible.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren, useState } from 'react'; 2 | import { StyleSheet, TouchableOpacity } from 'react-native'; 3 | 4 | import { ThemedText } from '@/components/ThemedText'; 5 | import { ThemedView } from '@/components/ThemedView'; 6 | import { IconSymbol } from '@/components/ui/IconSymbol'; 7 | import { Colors } from '@/constants/Colors'; 8 | import { useColorScheme } from '@/hooks/useColorScheme'; 9 | 10 | export function Collapsible({ children, title }: PropsWithChildren & { title: string }) { 11 | const [isOpen, setIsOpen] = useState(false); 12 | const theme = useColorScheme() ?? 'light'; 13 | 14 | return ( 15 | 16 | setIsOpen((value) => !value)} 19 | activeOpacity={0.8}> 20 | 27 | 28 | {title} 29 | 30 | {isOpen && {children}} 31 | 32 | ); 33 | } 34 | 35 | const styles = StyleSheet.create({ 36 | heading: { 37 | flexDirection: 'row', 38 | alignItems: 'center', 39 | gap: 6, 40 | }, 41 | content: { 42 | marginTop: 6, 43 | marginLeft: 24, 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /components/ThemedText.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Text, type TextProps } from 'react-native'; 2 | 3 | import { useThemeColor } from '@/hooks/useThemeColor'; 4 | 5 | export type ThemedTextProps = TextProps & { 6 | lightColor?: string; 7 | darkColor?: string; 8 | type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link'; 9 | }; 10 | 11 | export function ThemedText({ 12 | style, 13 | lightColor, 14 | darkColor, 15 | type = 'default', 16 | ...rest 17 | }: ThemedTextProps) { 18 | const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); 19 | 20 | return ( 21 | 33 | ); 34 | } 35 | 36 | const styles = StyleSheet.create({ 37 | default: { 38 | fontSize: 16, 39 | lineHeight: 24, 40 | }, 41 | defaultSemiBold: { 42 | fontSize: 16, 43 | lineHeight: 24, 44 | fontWeight: '600', 45 | }, 46 | title: { 47 | fontSize: 32, 48 | fontWeight: 'bold', 49 | lineHeight: 32, 50 | }, 51 | subtitle: { 52 | fontSize: 20, 53 | fontWeight: 'bold', 54 | }, 55 | link: { 56 | lineHeight: 30, 57 | fontSize: 16, 58 | color: '#0a7ea4', 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /components/ui/IconSymbol.tsx: -------------------------------------------------------------------------------- 1 | // Fallback for using MaterialIcons on Android and web. 2 | 3 | import MaterialIcons from '@expo/vector-icons/MaterialIcons'; 4 | import { SymbolWeight, SymbolViewProps } from 'expo-symbols'; 5 | import { ComponentProps } from 'react'; 6 | import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native'; 7 | 8 | type IconMapping = Record['name']>; 9 | type IconSymbolName = keyof typeof MAPPING; 10 | 11 | /** 12 | * Add your SF Symbols to Material Icons mappings here. 13 | * - see Material Icons in the [Icons Directory](https://icons.expo.fyi). 14 | * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app. 15 | */ 16 | const MAPPING = { 17 | 'house.fill': 'home', 18 | 'paperplane.fill': 'send', 19 | 'chevron.left.forwardslash.chevron.right': 'code', 20 | 'chevron.right': 'chevron-right', 21 | } as IconMapping; 22 | 23 | /** 24 | * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web. 25 | * This ensures a consistent look across platforms, and optimal resource usage. 26 | * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons. 27 | */ 28 | export function IconSymbol({ 29 | name, 30 | size = 24, 31 | color, 32 | style, 33 | }: { 34 | name: IconSymbolName; 35 | size?: number; 36 | color: string | OpaqueColorValue; 37 | style?: StyleProp; 38 | weight?: SymbolWeight; 39 | }) { 40 | return ; 41 | } 42 | -------------------------------------------------------------------------------- /ios/ReactExported/ReactNativeEventEmitter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactNativeEventEmitter.swift 3 | // 4 | // Created by Tiago Daniel Jacobs on 11/03/22. 5 | // 6 | 7 | import Foundation 8 | import React 9 | 10 | @objc(ReactNativeEventEmitter) 11 | open class ReactNativeEventEmitter: RCTEventEmitter { 12 | 13 | public static var emitter: RCTEventEmitter! 14 | 15 | public enum EVENT: String, CaseIterable { 16 | case onBroadcastRequested = "onBroadcastRequested" 17 | case onBroadcastStarted = "onBroadcastStarted" 18 | case onBroadcastPaused = "onBroadcastPaused" 19 | case onBroadcastResumed = "onBroadcastResumed" 20 | case onBroadcastFinished = "onBroadcastFinished" 21 | case onScreenShareOfferCreated = "onScreenShareOfferCreated" 22 | case onSetScreenShareRemoteSDPCompleted = "onSetScreenShareRemoteSDPCompleted" 23 | case onScreenShareLocalIceCandidate = "onScreenShareLocalIceCandidate" 24 | case onScreenShareSignalingStateChange = "onScreenShareSignalingStateChange" 25 | case onAddScreenShareRemoteIceCandidateCompleted = "onAddScreenShareRemoteIceCandidateCompleted" 26 | case onFullAudioOfferCreated = "onFullAudioOfferCreated" 27 | case onSetFullAudioRemoteSDPCompleted = "onSetFullAudioRemoteSDPCompleted" 28 | } 29 | 30 | override init() { 31 | super.init() 32 | ReactNativeEventEmitter.emitter = self 33 | } 34 | 35 | open override func supportedEvents() -> [String] { 36 | EVENT.allCases.map { $0.rawValue } 37 | } 38 | 39 | @objc open override class func requiresMainQueueSetup() -> Bool { 40 | return false 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to BigBlueButton Tablet app 👋 2 | 3 | BigBlueButton normally runs in a web browser. However, on iOS, browser-based screen sharing is not supported due to system limitations. This app solves that by embedding BigBlueButton in a native webview, allowing you to **share your screen on iOS devices**—something not possible with Safari or other browsers. 4 | 5 | In addition to screen sharing, the app also provides **improved background audio support**, enhancing the overall meeting experience. 6 | 7 | > **Note:** Although the app works on mobile phones, it is primarily optimized for tablets. Because it uses a webview to render the BigBlueButton interface, a device with a **strong CPU** is recommended for best performance. 8 | 9 | ## Use the app 10 | 11 | The app is available on Apple App Store. 12 | 13 | ## Run from source 14 | 15 | 1. Ensure you are not using latest Xcode 16 | 17 | Cocoa pods was not working with latest Xcode. 18 | We downgrade it to 16.0 to get it working. 19 | ([Details](https://github.com/CocoaPods/CocoaPods/issues/12794)) 20 | 21 | 2. Install ios dependencies 22 | 23 | ```bash 24 | cd ios 25 | pod install 26 | ``` 27 | 28 | 3. Open the project in Xcode 29 | 30 | ```bash 31 | open ios/bigbluebuttontablet.xcworkspace 32 | ``` 33 | 34 | 35 | 4. Install javascript 36 | 37 | ```bash 38 | npm install 39 | ``` 40 | 41 | 5. Start the app 42 | 43 | ```bash 44 | npx expo start 45 | ``` 46 | 47 | ## Internationalization (i18n) 48 | 49 | This project uses [react-i18next](https://react.i18next.com/), [i18next](https://www.i18next.com/), and [expo-localization](https://docs.expo.dev/versions/latest/sdk/localization/) for internationalization. Translation files are located in the `i18n/locales` directory. Supported languages include English (en), Brazilian Portuguese (pt-BR), and German (de). 50 | 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bigbluebutton-tablet", 3 | "main": "expo-router/entry", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "expo start", 7 | "reset-project": "node ./scripts/reset-project.js", 8 | "android": "expo run:android", 9 | "ios": "expo run:ios", 10 | "web": "expo start --web", 11 | "lint": "expo lint", 12 | "i18n:check": "echo 'Check i18n keys with your preferred tool'" 13 | }, 14 | "dependencies": { 15 | "@expo/vector-icons": "^14.1.0", 16 | "@react-native-async-storage/async-storage": "2.1.2", 17 | "@react-native-picker/picker": "^2.11.1", 18 | "@react-navigation/bottom-tabs": "^7.3.10", 19 | "@react-navigation/elements": "^2.3.8", 20 | "@react-navigation/native": "^7.1.6", 21 | "expo": "~53.0.9", 22 | "expo-blur": "~14.1.4", 23 | "expo-constants": "~17.1.6", 24 | "expo-font": "~13.3.1", 25 | "expo-haptics": "~14.1.4", 26 | "expo-image": "~2.1.7", 27 | "expo-linking": "~7.1.5", 28 | "expo-localization": "~16.1.6", 29 | "expo-router": "~5.0.6", 30 | "expo-splash-screen": "~0.30.8", 31 | "expo-status-bar": "~2.2.3", 32 | "expo-symbols": "~0.4.4", 33 | "expo-system-ui": "~5.0.7", 34 | "expo-web-browser": "~14.1.6", 35 | "i18next": "^25.3.1", 36 | "react": "19.0.0", 37 | "react-dom": "19.0.0", 38 | "react-i18next": "^15.6.0", 39 | "react-native": "0.79.2", 40 | "react-native-draggable": "^3.3.0", 41 | "react-native-gesture-handler": "~2.24.0", 42 | "react-native-reanimated": "~3.17.4", 43 | "react-native-safe-area-context": "5.4.0", 44 | "react-native-screens": "~4.10.0", 45 | "react-native-web": "~0.20.0", 46 | "react-native-webview": "^13.13.5" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.25.2", 50 | "@types/react": "~19.0.10", 51 | "eslint": "^9.25.0", 52 | "eslint-config-expo": "~9.2.0", 53 | "typescript": "~5.8.3" 54 | }, 55 | "private": true 56 | } 57 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 22 | 23 | 24 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/anonymous/bigbluebuttontablet/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.anonymous.bigbluebuttontablet 2 | 3 | import android.app.Application 4 | import android.content.res.Configuration 5 | 6 | import com.facebook.react.PackageList 7 | import com.facebook.react.ReactApplication 8 | import com.facebook.react.ReactNativeHost 9 | import com.facebook.react.ReactPackage 10 | import com.facebook.react.ReactHost 11 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load 12 | import com.facebook.react.defaults.DefaultReactNativeHost 13 | import com.facebook.react.soloader.OpenSourceMergedSoMapping 14 | import com.facebook.soloader.SoLoader 15 | 16 | import expo.modules.ApplicationLifecycleDispatcher 17 | import expo.modules.ReactNativeHostWrapper 18 | 19 | class MainApplication : Application(), ReactApplication { 20 | 21 | override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper( 22 | this, 23 | object : DefaultReactNativeHost(this) { 24 | override fun getPackages(): List { 25 | val packages = PackageList(this).packages 26 | // Packages that cannot be autolinked yet can be added manually here, for example: 27 | // packages.add(MyReactNativePackage()) 28 | return packages 29 | } 30 | 31 | override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry" 32 | 33 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG 34 | 35 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 36 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED 37 | } 38 | ) 39 | 40 | override val reactHost: ReactHost 41 | get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost) 42 | 43 | override fun onCreate() { 44 | super.onCreate() 45 | SoLoader.init(this, OpenSourceMergedSoMapping) 46 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 47 | // If you opted-in for the New Architecture, we load the native entry point for this app. 48 | load() 49 | } 50 | ApplicationLifecycleDispatcher.onApplicationCreate(this) 51 | } 52 | 53 | override fun onConfigurationChanged(newConfig: Configuration) { 54 | super.onConfigurationChanged(newConfig) 55 | ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /components/ParallaxScrollView.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren, ReactElement } from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | import Animated, { 4 | interpolate, 5 | useAnimatedRef, 6 | useAnimatedStyle, 7 | useScrollViewOffset, 8 | } from 'react-native-reanimated'; 9 | 10 | import { ThemedView } from '@/components/ThemedView'; 11 | import { useBottomTabOverflow } from '@/components/ui/TabBarBackground'; 12 | import { useColorScheme } from '@/hooks/useColorScheme'; 13 | 14 | const HEADER_HEIGHT = 250; 15 | 16 | type Props = PropsWithChildren<{ 17 | headerImage: ReactElement; 18 | headerBackgroundColor: { dark: string; light: string }; 19 | }>; 20 | 21 | export default function ParallaxScrollView({ 22 | children, 23 | headerImage, 24 | headerBackgroundColor, 25 | }: Props) { 26 | const colorScheme = useColorScheme() ?? 'light'; 27 | const scrollRef = useAnimatedRef(); 28 | const scrollOffset = useScrollViewOffset(scrollRef); 29 | const bottom = useBottomTabOverflow(); 30 | const headerAnimatedStyle = useAnimatedStyle(() => { 31 | return { 32 | transform: [ 33 | { 34 | translateY: interpolate( 35 | scrollOffset.value, 36 | [-HEADER_HEIGHT, 0, HEADER_HEIGHT], 37 | [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75] 38 | ), 39 | }, 40 | { 41 | scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]), 42 | }, 43 | ], 44 | }; 45 | }); 46 | 47 | return ( 48 | 49 | 54 | 60 | {headerImage} 61 | 62 | {children} 63 | 64 | 65 | ); 66 | } 67 | 68 | const styles = StyleSheet.create({ 69 | container: { 70 | flex: 1, 71 | }, 72 | header: { 73 | height: HEADER_HEIGHT, 74 | overflow: 'hidden', 75 | }, 76 | content: { 77 | flex: 1, 78 | padding: 32, 79 | gap: 16, 80 | overflow: 'hidden', 81 | }, 82 | }); 83 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | 25 | # Enable AAPT2 PNG crunching 26 | android.enablePngCrunchInReleaseBuilds=true 27 | 28 | # Use this property to specify which architecture you want to build. 29 | # You can also override it from the CLI using 30 | # ./gradlew -PreactNativeArchitectures=x86_64 31 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 32 | 33 | # Use this property to enable support to the new architecture. 34 | # This will allow you to use TurboModules and the Fabric render in 35 | # your application. You should enable this flag either if you want 36 | # to write custom TurboModules/Fabric components OR use libraries that 37 | # are providing them. 38 | newArchEnabled=true 39 | 40 | # Use this property to enable or disable the Hermes JS engine. 41 | # If set to false, you will be using JSC instead. 42 | hermesEnabled=true 43 | 44 | # Enable GIF support in React Native images (~200 B increase) 45 | expo.gif.enabled=true 46 | # Enable webp support in React Native images (~85 KB increase) 47 | expo.webp.enabled=true 48 | # Enable animated webp support (~3.4 MB increase) 49 | # Disabled by default because iOS doesn't support animated webp 50 | expo.webp.animated=false 51 | 52 | # Enable network inspector 53 | EX_DEV_CLIENT_NETWORK_INSPECTOR=true 54 | 55 | # Use legacy packaging to compress native libraries in the resulting APK. 56 | expo.useLegacyPackaging=false 57 | 58 | # Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin 59 | expo.edgeToEdgeEnabled=true -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") 2 | require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods") 3 | 4 | require 'json' 5 | podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} 6 | 7 | ENV['RCT_NEW_ARCH_ENABLED'] = '0' if podfile_properties['newArchEnabled'] == 'false' 8 | ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR'] 9 | 10 | platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1' 11 | install! 'cocoapods', 12 | :deterministic_uuids => false 13 | 14 | prepare_react_native_project! 15 | 16 | target 'bigbluebuttontablet' do 17 | use_expo_modules! 18 | 19 | pod 'WebRTC-lib' 20 | 21 | if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' 22 | config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; 23 | else 24 | config_command = [ 25 | 'npx', 26 | 'expo-modules-autolinking', 27 | 'react-native-config', 28 | '--json', 29 | '--platform', 30 | 'ios' 31 | ] 32 | end 33 | 34 | config = use_native_modules!(config_command) 35 | 36 | use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] 37 | use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] 38 | 39 | use_react_native!( 40 | :path => config[:reactNativePath], 41 | :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes', 42 | # An absolute path to your application root. 43 | :app_path => "#{Pod::Config.instance.installation_root}/..", 44 | :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false', 45 | ) 46 | 47 | post_install do |installer| 48 | react_native_post_install( 49 | installer, 50 | config[:reactNativePath], 51 | :mac_catalyst_enabled => false, 52 | :ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true', 53 | ) 54 | 55 | # This is necessary for Xcode 14, because it signs resource bundles by default 56 | # when building for devices. 57 | installer.target_installation_results.pod_target_installation_results 58 | .each do |pod_name, target_installation_result| 59 | target_installation_result.resource_bundle_targets.each do |resource_bundle_target| 60 | resource_bundle_target.build_configurations.each do |config| 61 | config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/anonymous/bigbluebuttontablet/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.anonymous.bigbluebuttontablet 2 | import expo.modules.splashscreen.SplashScreenManager 3 | 4 | import android.os.Build 5 | import android.os.Bundle 6 | 7 | import com.facebook.react.ReactActivity 8 | import com.facebook.react.ReactActivityDelegate 9 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 10 | import com.facebook.react.defaults.DefaultReactActivityDelegate 11 | 12 | import expo.modules.ReactActivityDelegateWrapper 13 | 14 | class MainActivity : ReactActivity() { 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | // Set the theme to AppTheme BEFORE onCreate to support 17 | // coloring the background, status bar, and navigation bar. 18 | // This is required for expo-splash-screen. 19 | // setTheme(R.style.AppTheme); 20 | // @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af 21 | SplashScreenManager.registerOnActivity(this) 22 | // @generated end expo-splashscreen 23 | super.onCreate(null) 24 | } 25 | 26 | /** 27 | * Returns the name of the main component registered from JavaScript. This is used to schedule 28 | * rendering of the component. 29 | */ 30 | override fun getMainComponentName(): String = "main" 31 | 32 | /** 33 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] 34 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] 35 | */ 36 | override fun createReactActivityDelegate(): ReactActivityDelegate { 37 | return ReactActivityDelegateWrapper( 38 | this, 39 | BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, 40 | object : DefaultReactActivityDelegate( 41 | this, 42 | mainComponentName, 43 | fabricEnabled 44 | ){}) 45 | } 46 | 47 | /** 48 | * Align the back button behavior with Android S 49 | * where moving root activities to background instead of finishing activities. 50 | * @see onBackPressed 51 | */ 52 | override fun invokeDefaultOnBackPressed() { 53 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { 54 | if (!moveTaskToBack(false)) { 55 | // For non-root activities, use the default implementation to finish them. 56 | super.invokeDefaultOnBackPressed() 57 | } 58 | return 59 | } 60 | 61 | // Use the default back button implementation on Android S 62 | // because it's doing more than [Activity.moveTaskToBack] in fact. 63 | super.invokeDefaultOnBackPressed() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/webview/message-handler.tsx: -------------------------------------------------------------------------------- 1 | import type { MutableRefObject } from 'react'; 2 | import type { WebView, WebViewMessageEvent } from 'react-native-webview'; 3 | import addScreenShareRemoteIceCandidate from '../methods/addScreenShareRemoteIceCandidate'; 4 | import createScreenShareOffer from '../methods/createScreenShareOffer'; 5 | import initializeScreenShare from '../methods/initializeScreenShare'; 6 | import setScreenShareRemoteSDP from '../methods/setScreenShareRemoteSDP'; 7 | import stopScreenShare from '../methods/stopScreenShare'; 8 | 9 | function observePromiseResult( 10 | instanceId: Number, 11 | webViewRef: MutableRefObject, 12 | sequence: number, 13 | prom: Promise 14 | ) { 15 | prom 16 | .then((result: any) => { 17 | console.log(`[${instanceId}] - Promise ${sequence} resolved!`, result); 18 | webViewRef.current.injectJavaScript( 19 | `window.nativeMethodCallResult(${sequence}, true ${ 20 | result ? ',' + JSON.stringify(result) : '' 21 | });` 22 | ); 23 | }) 24 | .catch((exception: any) => { 25 | console.error(`[${instanceId}] - Promise ${sequence} failed!`, exception); 26 | webViewRef.current.injectJavaScript( 27 | `window.nativeMethodCallResult(${sequence}, false ${ 28 | exception ? ',' + JSON.stringify(exception) : '' 29 | });` 30 | ); 31 | }); 32 | } 33 | 34 | export function handleWebviewMessage( 35 | instanceId: Number, 36 | webViewRef: MutableRefObject, 37 | event: WebViewMessageEvent 38 | ) { 39 | const stringData = event?.nativeEvent?.data; 40 | 41 | console.log('handleWebviewMessage - ', instanceId); 42 | 43 | const data = JSON.parse(stringData); 44 | if (data?.method && data?.sequence) { 45 | let promise; 46 | switch (data?.method) { 47 | case 'initializeScreenShare': 48 | promise = initializeScreenShare(instanceId); 49 | break; 50 | case 'createScreenShareOffer': 51 | promise = createScreenShareOffer( 52 | instanceId, 53 | JSON.stringify(data?.arguments[0]) 54 | ); 55 | break; 56 | case 'setScreenShareRemoteSDP': 57 | promise = setScreenShareRemoteSDP(instanceId, data?.arguments[0].sdp); 58 | break; 59 | case 'addRemoteIceCandidate': 60 | promise = addScreenShareRemoteIceCandidate( 61 | instanceId, 62 | JSON.stringify(data?.arguments[0]) 63 | ); 64 | break; 65 | case 'stopScreenShare': 66 | promise = stopScreenShare(instanceId); 67 | break; 68 | default: 69 | throw `[${instanceId}] - Unknown method ${data?.method}`; 70 | } 71 | observePromiseResult(instanceId, webViewRef, data.sequence, promise); 72 | } else { 73 | console.log(`[${instanceId}] - Ignoring unknown message: $stringData`); 74 | } 75 | } 76 | 77 | export default { handleWebviewMessage }; 78 | -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/SplashScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /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 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /ios/bigbluebuttontablet/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | BigBlueButton Tablet 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | 2.2.0 23 | CFBundleSignature 24 | ???? 25 | CFBundleURLTypes 26 | 27 | 28 | CFBundleURLSchemes 29 | 30 | bigbluebuttontablet 31 | com.anonymous.bigbluebuttontablet 32 | 33 | 34 | 35 | CFBundleVersion 36 | 1 37 | LSMinimumSystemVersion 38 | 12.0 39 | LSRequiresIPhoneOS 40 | 41 | NSAppTransportSecurity 42 | 43 | NSAllowsArbitraryLoads 44 | 45 | NSAllowsLocalNetworking 46 | 47 | 48 | NSCameraUsageDescription 49 | We will capture your camera if you join with video. 50 | NSMicrophoneUsageDescription 51 | We will capture your microphone if you join with full audio. 52 | NSUserActivityTypes 53 | 54 | $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route 55 | 56 | UIBackgroundModes 57 | 58 | audio 59 | 60 | UILaunchStoryboardName 61 | SplashScreen 62 | UIRequiredDeviceCapabilities 63 | 64 | arm64 65 | 66 | UIRequiresFullScreen 67 | 68 | UIStatusBarStyle 69 | UIStatusBarStyleDefault 70 | UISupportedInterfaceOrientations 71 | 72 | UIInterfaceOrientationPortrait 73 | UIInterfaceOrientationPortraitUpsideDown 74 | 75 | UISupportedInterfaceOrientations~ipad 76 | 77 | UIInterfaceOrientationPortrait 78 | UIInterfaceOrientationPortraitUpsideDown 79 | UIInterfaceOrientationLandscapeLeft 80 | UIInterfaceOrientationLandscapeRight 81 | 82 | UIUserInterfaceStyle 83 | Automatic 84 | UIViewControllerBasedStatusBarAppearance 85 | 86 | NSCameraUsageDescription 87 | We will capture your camera if you join with video. 88 | NSMicrophoneUsageDescription 89 | We will capture your microphone if you join with full audio. 90 | UIBackgroundModes 91 | 92 | audio 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /ios/bigbluebuttontablet.xcodeproj/xcshareddata/xcschemes/bigbluebuttontablet.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /scripts/reset-project.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script is used to reset the project to a blank state. 5 | * It deletes or moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example based on user input and creates a new /app directory with an index.tsx and _layout.tsx file. 6 | * You can remove the `reset-project` script from package.json and safely delete this file after running it. 7 | */ 8 | 9 | const fs = require("fs"); 10 | const path = require("path"); 11 | const readline = require("readline"); 12 | 13 | const root = process.cwd(); 14 | const oldDirs = ["app", "components", "hooks", "constants", "scripts"]; 15 | const exampleDir = "app-example"; 16 | const newAppDir = "app"; 17 | const exampleDirPath = path.join(root, exampleDir); 18 | 19 | const indexContent = `import { Text, View } from "react-native"; 20 | 21 | export default function Index() { 22 | return ( 23 | 30 | Edit app/index.tsx to edit this screen. 31 | 32 | ); 33 | } 34 | `; 35 | 36 | const layoutContent = `import { Stack } from "expo-router"; 37 | 38 | export default function RootLayout() { 39 | return ; 40 | } 41 | `; 42 | 43 | const rl = readline.createInterface({ 44 | input: process.stdin, 45 | output: process.stdout, 46 | }); 47 | 48 | const moveDirectories = async (userInput) => { 49 | try { 50 | if (userInput === "y") { 51 | // Create the app-example directory 52 | await fs.promises.mkdir(exampleDirPath, { recursive: true }); 53 | console.log(`📁 /${exampleDir} directory created.`); 54 | } 55 | 56 | // Move old directories to new app-example directory or delete them 57 | for (const dir of oldDirs) { 58 | const oldDirPath = path.join(root, dir); 59 | if (fs.existsSync(oldDirPath)) { 60 | if (userInput === "y") { 61 | const newDirPath = path.join(root, exampleDir, dir); 62 | await fs.promises.rename(oldDirPath, newDirPath); 63 | console.log(`➡️ /${dir} moved to /${exampleDir}/${dir}.`); 64 | } else { 65 | await fs.promises.rm(oldDirPath, { recursive: true, force: true }); 66 | console.log(`❌ /${dir} deleted.`); 67 | } 68 | } else { 69 | console.log(`➡️ /${dir} does not exist, skipping.`); 70 | } 71 | } 72 | 73 | // Create new /app directory 74 | const newAppDirPath = path.join(root, newAppDir); 75 | await fs.promises.mkdir(newAppDirPath, { recursive: true }); 76 | console.log("\n📁 New /app directory created."); 77 | 78 | // Create index.tsx 79 | const indexPath = path.join(newAppDirPath, "index.tsx"); 80 | await fs.promises.writeFile(indexPath, indexContent); 81 | console.log("📄 app/index.tsx created."); 82 | 83 | // Create _layout.tsx 84 | const layoutPath = path.join(newAppDirPath, "_layout.tsx"); 85 | await fs.promises.writeFile(layoutPath, layoutContent); 86 | console.log("📄 app/_layout.tsx created."); 87 | 88 | console.log("\n✅ Project reset complete. Next steps:"); 89 | console.log( 90 | `1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${ 91 | userInput === "y" 92 | ? `\n3. Delete the /${exampleDir} directory when you're done referencing it.` 93 | : "" 94 | }` 95 | ); 96 | } catch (error) { 97 | console.error(`❌ Error during script execution: ${error.message}`); 98 | } 99 | }; 100 | 101 | rl.question( 102 | "Do you want to move existing files to /app-example instead of deleting them? (Y/n): ", 103 | (answer) => { 104 | const userInput = answer.trim().toLowerCase() || "y"; 105 | if (userInput === "y" || userInput === "n") { 106 | moveDirectories(userInput).finally(() => rl.close()); 107 | } else { 108 | console.log("❌ Invalid input. Please enter 'Y' or 'N'."); 109 | rl.close(); 110 | } 111 | } 112 | ); 113 | -------------------------------------------------------------------------------- /ios/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/CocoaPods/CocoaPods.git 3 | revision: 648ccdcaea2063fe63977a0146e1717aec3efa54 4 | branch: master 5 | specs: 6 | cocoapods (1.16.2) 7 | addressable (~> 2.8) 8 | claide (>= 1.0.2, < 2.0) 9 | cocoapods-core (= 1.16.2) 10 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 11 | cocoapods-downloader (>= 2.1, < 3.0) 12 | cocoapods-plugins (>= 1.0.0, < 2.0) 13 | cocoapods-search (>= 1.0.0, < 2.0) 14 | cocoapods-trunk (>= 1.6.0, < 2.0) 15 | cocoapods-try (>= 1.1.0, < 2.0) 16 | colored2 (~> 3.1) 17 | escape (~> 0.0.4) 18 | fourflusher (>= 2.3.0, < 3.0) 19 | gh_inspector (~> 1.0) 20 | molinillo (~> 0.8.0) 21 | nap (~> 1.0) 22 | ruby-macho (~> 4.1.0) 23 | xcodeproj (>= 1.27.0, < 2.0) 24 | 25 | GIT 26 | remote: https://github.com/CocoaPods/Xcodeproj.git 27 | revision: ab3dfa504b5a97cae3a653a8924f4616dcaa062e 28 | branch: master 29 | specs: 30 | xcodeproj (1.27.0) 31 | CFPropertyList (>= 2.3.3, < 4.0) 32 | atomos (~> 0.1.3) 33 | claide (>= 1.0.2, < 2.0) 34 | colored2 (~> 3.1) 35 | nanaimo (~> 0.4.0) 36 | rexml (>= 3.3.6, < 4.0) 37 | 38 | GEM 39 | remote: https://rubygems.org/ 40 | specs: 41 | CFPropertyList (3.0.7) 42 | base64 43 | nkf 44 | rexml 45 | activesupport (7.2.2.1) 46 | base64 47 | benchmark (>= 0.3) 48 | bigdecimal 49 | concurrent-ruby (~> 1.0, >= 1.3.1) 50 | connection_pool (>= 2.2.5) 51 | drb 52 | i18n (>= 1.6, < 2) 53 | logger (>= 1.4.2) 54 | minitest (>= 5.1) 55 | securerandom (>= 0.3) 56 | tzinfo (~> 2.0, >= 2.0.5) 57 | addressable (2.8.7) 58 | public_suffix (>= 2.0.2, < 7.0) 59 | algoliasearch (1.27.5) 60 | httpclient (~> 2.8, >= 2.8.3) 61 | json (>= 1.5.1) 62 | atomos (0.1.3) 63 | base64 (0.3.0) 64 | benchmark (0.4.1) 65 | bigdecimal (3.2.2) 66 | claide (1.1.0) 67 | cocoapods-core (1.16.2) 68 | activesupport (>= 5.0, < 8) 69 | addressable (~> 2.8) 70 | algoliasearch (~> 1.0) 71 | concurrent-ruby (~> 1.1) 72 | fuzzy_match (~> 2.0.4) 73 | nap (~> 1.0) 74 | netrc (~> 0.11) 75 | public_suffix (~> 4.0) 76 | typhoeus (~> 1.0) 77 | cocoapods-deintegrate (1.0.5) 78 | cocoapods-downloader (2.1) 79 | cocoapods-plugins (1.0.0) 80 | nap 81 | cocoapods-search (1.0.1) 82 | cocoapods-trunk (1.6.0) 83 | nap (>= 0.8, < 2.0) 84 | netrc (~> 0.11) 85 | cocoapods-try (1.2.0) 86 | colored2 (3.1.2) 87 | concurrent-ruby (1.3.5) 88 | connection_pool (2.5.3) 89 | drb (2.2.3) 90 | escape (0.0.4) 91 | ethon (0.16.0) 92 | ffi (>= 1.15.0) 93 | ffi (1.17.2) 94 | ffi (1.17.2-aarch64-linux-gnu) 95 | ffi (1.17.2-aarch64-linux-musl) 96 | ffi (1.17.2-arm-linux-gnu) 97 | ffi (1.17.2-arm-linux-musl) 98 | ffi (1.17.2-arm64-darwin) 99 | ffi (1.17.2-x86-linux-gnu) 100 | ffi (1.17.2-x86-linux-musl) 101 | ffi (1.17.2-x86_64-darwin) 102 | ffi (1.17.2-x86_64-linux-gnu) 103 | ffi (1.17.2-x86_64-linux-musl) 104 | fourflusher (2.3.1) 105 | fuzzy_match (2.0.4) 106 | gh_inspector (1.1.3) 107 | httpclient (2.9.0) 108 | mutex_m 109 | i18n (1.14.7) 110 | concurrent-ruby (~> 1.0) 111 | json (2.12.2) 112 | logger (1.7.0) 113 | minitest (5.25.5) 114 | molinillo (0.8.0) 115 | mutex_m (0.3.0) 116 | nanaimo (0.4.0) 117 | nap (1.1.0) 118 | netrc (0.11.0) 119 | nkf (0.2.0) 120 | public_suffix (4.0.7) 121 | rexml (3.4.1) 122 | ruby-macho (4.1.0) 123 | securerandom (0.4.1) 124 | typhoeus (1.4.1) 125 | ethon (>= 0.9.0) 126 | tzinfo (2.0.6) 127 | concurrent-ruby (~> 1.0) 128 | 129 | PLATFORMS 130 | aarch64-linux-gnu 131 | aarch64-linux-musl 132 | arm-linux-gnu 133 | arm-linux-musl 134 | arm64-darwin 135 | ruby 136 | x86-linux-gnu 137 | x86-linux-musl 138 | x86_64-darwin 139 | x86_64-linux-gnu 140 | x86_64-linux-musl 141 | 142 | DEPENDENCIES 143 | cocoapods! 144 | xcodeproj! 145 | 146 | BUNDLED WITH 147 | 2.6.7 148 | -------------------------------------------------------------------------------- /ios/ScreenSharing/ScreenSharePublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenSharePublisher.swift 3 | // bigbluebuttontablet 4 | // 5 | // Created by Tiago Daniel Jacobs, 2025 6 | // 7 | 8 | import Foundation 9 | import UIKit // For UIImage & UIActivityViewController 10 | import CoreImage // For CIImage & CIContext 11 | 12 | /// A screen-sharing component that listens for new video frames, 13 | /// detects the start of broadcasting, logs metadata, and relays frames 14 | /// to the broadcasting service. 15 | final class ScreenSharePublisher { 16 | 17 | // MARK: - Private State 18 | 19 | /// A dedicated queue for deserialization and processing tasks (low-priority utility queue). 20 | private static let queue = DispatchQueue(label: "hello.printer", qos: .utility) 21 | 22 | /// Shared timer for periodic frame polling. 23 | private static var timer: DispatchSourceTimer? 24 | 25 | /// Tracks if we’re actively broadcasting (i.e., received non-clean frames). 26 | /// On transition from `false → true`, "Broadcast started" is logged once. 27 | private static var broadcastActive = false 28 | 29 | // MARK: - Public API 30 | 31 | /// Starts the screen-share monitoring logic with a ~30Hz polling timer. 32 | static func start() { 33 | // Create the dispatch timer 34 | let t = DispatchSource.makeTimerSource(queue: queue) 35 | t.schedule( 36 | deadline: .now(), 37 | repeating: .milliseconds(33), // ≈ 30Hz 38 | leeway: .milliseconds(1) // Slight flexibility is acceptable 39 | ) 40 | 41 | // Reset shared frame memory and internal state 42 | IPCCurrentVideoFrame.shared.clear() 43 | broadcastActive = false 44 | 45 | // Define timer's work block 46 | t.setEventHandler { 47 | // 1️⃣ Attempt to fetch the latest frame data 48 | guard let data = IPCCurrentVideoFrame.shared.get() else { 49 | return // No frame data yet 50 | } 51 | 52 | // 2️⃣ Determine if the buffer is clean (no visual change) 53 | let isClean = IPCCurrentVideoFrame.shared.isClean() 54 | 55 | // 3️⃣ Detect transition into broadcasting 56 | if !isClean && !broadcastActive { 57 | broadcastActive = true 58 | print("Broadcast started") 59 | ReactNativeEventEmitter.emitter.sendEvent( 60 | withName: ReactNativeEventEmitter.EVENT.onBroadcastStarted.rawValue, 61 | body: nil 62 | ) 63 | } else if isClean && broadcastActive { 64 | // Reset state when returning to clean 65 | broadcastActive = false 66 | } 67 | 68 | // 4️⃣ Skip frame if there's no new content 69 | guard !isClean else { 70 | return 71 | } 72 | 73 | // 5️⃣ Attempt to deserialize the pixel buffer and log details 74 | do { 75 | // Destructure the returned tuple into buffer, orientation, and metadata 76 | let (buffer, orientation, header) = try deserializePixelBufferFull(data) 77 | 78 | // Extract and (optionally) log the height for diagnostics 79 | let height = CVPixelBufferGetHeight(buffer) 80 | // print("Frame height: \(height)") 81 | // print("Decoded timestamp: \(header.timestampNs)") 82 | 83 | // 6️⃣ Pass the frame to the broadcasting service 84 | ScreenBroadcasterService.shared.pushVideoFrame( 85 | timeStampNs: header.timestampNs, 86 | orientation: orientation, 87 | imageBuffer: buffer 88 | ) 89 | 90 | } catch { 91 | // Handle any deserialization error (with detailed message) 92 | print("Failed to deserialize pixel buffer – \(error)") 93 | } 94 | } 95 | 96 | // Start the timer and retain a reference to prevent deallocation 97 | t.resume() 98 | timer = t 99 | } 100 | 101 | /// Stops the polling and resets the broadcast state. 102 | static func stop() { 103 | timer?.cancel() 104 | timer = nil 105 | broadcastActive = false // Clean up internal state 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /ios/BigBlueButton Screen Share/SampleHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleHandler.swift 3 | // BigBlueButton Screen Share 4 | // 5 | // Created by Tiago Daniel Jacobs on 22/05/25. 6 | // 7 | 8 | // Debug flags 9 | struct DebugFlags { 10 | static var stopTimer = false 11 | static var videoFrames = false 12 | static var audioApp = false 13 | static var audioMic = false 14 | } 15 | 16 | @inline(__always) 17 | func dlog(_ enabled: @autoclosure () -> Bool, _ message: @autoclosure () -> String) { 18 | if enabled() { print(message()) } 19 | } 20 | 21 | class SampleHandler: RPBroadcastSampleHandler { 22 | 23 | private var stopMonitorTimer: DispatchSourceTimer? 24 | 25 | override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) { 26 | dlog(DebugFlags.stopTimer, "Broadcast started") 27 | IPCCurrentVideoFrame.shared.clear() 28 | 29 | let queue = DispatchQueue(label: "org.bigbluebutton.tablet.stop-monitor") 30 | stopMonitorTimer = DispatchSource.makeTimerSource(queue: queue) 31 | stopMonitorTimer?.schedule(deadline: .now() + 1, repeating: 1) 32 | 33 | stopMonitorTimer?.setEventHandler { [weak self] in 34 | guard let strongSelf = self else { 35 | dlog(DebugFlags.stopTimer, "stop timer – strongSelf nil") 36 | self?.stopMonitorTimer?.cancel() 37 | return 38 | } 39 | dlog(DebugFlags.stopTimer, "stop timer – running") 40 | 41 | let userDefaults = UserDefaults(suiteName: "group.org.bigbluebutton.tablet") 42 | 43 | if userDefaults?.bool(forKey: "stopBroadcast") == true { 44 | dlog(DebugFlags.stopTimer, "stop timer – stopBroadcast=true") 45 | finishBroadcastGracefully(strongSelf) 46 | userDefaults?.set(false, forKey: "stopBroadcast") 47 | userDefaults?.synchronize() 48 | strongSelf.stopMonitorTimer?.cancel() 49 | return 50 | } else { 51 | dlog(DebugFlags.stopTimer, "stop timer – stopBroadcast=false") 52 | } 53 | 54 | if let last = userDefaults?.double(forKey: "mainAppHeartBeat") { 55 | if last > 0 { 56 | if Date().timeIntervalSince1970 - last > 3 { 57 | dlog(DebugFlags.stopTimer, "stop timer – heart stopped beating (last = \(Date(timeIntervalSince1970: last)))") 58 | // finishBroadcastGracefully(strongSelf) 59 | } else { 60 | dlog(DebugFlags.stopTimer, "stop timer – heart is still beating") 61 | } 62 | } else { 63 | dlog(DebugFlags.stopTimer, "stop timer – no heart beat yet (last = 0)") 64 | } 65 | } else { 66 | dlog(DebugFlags.stopTimer, "stop timer – no heart beat yet") 67 | } 68 | } 69 | 70 | stopMonitorTimer?.resume() 71 | } 72 | 73 | override func broadcastFinished() { 74 | stopMonitorTimer?.cancel() 75 | stopMonitorTimer = nil 76 | dlog(DebugFlags.stopTimer, "Broadcast finished") 77 | } 78 | 79 | override func broadcastPaused() { 80 | dlog(DebugFlags.stopTimer, "Broadcast paused") 81 | } 82 | 83 | override func broadcastResumed() { 84 | dlog(DebugFlags.stopTimer, "Broadcast resumed") 85 | } 86 | 87 | override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) { 88 | switch sampleBufferType { 89 | case .video: 90 | dlog(DebugFlags.videoFrames, "Video sample – begin") 91 | 92 | guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { 93 | dlog(DebugFlags.videoFrames, "Video sample – skip 1") 94 | return 95 | } 96 | 97 | var orientation = CGImagePropertyOrientation.up 98 | if let o = CMGetAttachment(sampleBuffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil) as? NSNumber { 99 | orientation = CGImagePropertyOrientation(rawValue: o.uint32Value) ?? .up 100 | } 101 | 102 | let timestampNs = Int64(CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) * 1_000_000_000) 103 | print("Encoded timestamp: \(timestampNs)") 104 | 105 | guard let data = serializePixelBufferFull(pixelBuffer: pixelBuffer, orientation: orientation, timestampNs:timestampNs) else { 106 | dlog(DebugFlags.videoFrames, "Video sample – skip 2") 107 | return 108 | } 109 | 110 | IPCCurrentVideoFrame.shared.set(data) 111 | dlog(DebugFlags.videoFrames, "Video sample – end") 112 | 113 | case .audioApp: 114 | dlog(DebugFlags.audioApp, "App audio sample") 115 | 116 | case .audioMic: 117 | dlog(DebugFlags.audioMic, "Mic audio sample") 118 | 119 | @unknown default: 120 | fatalError("Unknown type of sample buffer") 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /ios/ReactExported/ReactNativeScreenShareService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactNativeScreenShareService.swift 3 | // 4 | // Created by Tiago Daniel Jacobs on 11/03/22. 5 | // 6 | 7 | import Foundation 8 | import os 9 | import AVFAudio 10 | 11 | @objc(ReactNativeScreenShareService) 12 | class ScreenShareService: NSObject { 13 | // Logger (these messages are displayed in the console application) 14 | private var logger = os.Logger(subsystem: "BigBlueButtonTabletSDK", category: "ScreenShareServiceManager") 15 | var audioSession = AVAudioSession.sharedInstance() 16 | var player: AVAudioPlayer! 17 | 18 | // React native exposed method (called when user click the button to share screen) 19 | @objc func initializeScreenShare() -> Void { 20 | logger.info("initializeScreenShare") 21 | 22 | let userDefaults = UserDefaults(suiteName: "group.org.bigbluebutton.tablet") 23 | userDefaults?.set(false, forKey: "stopBroadcast") 24 | userDefaults?.synchronize() 25 | 26 | // Play audio in loop, to keep app active 27 | self.activeAudioSession(bool: true) 28 | let path = Bundle.main.path(forResource: "music2", ofType : "mp3")! 29 | let url = URL(fileURLWithPath : path) 30 | do { 31 | 32 | self.player = try AVAudioPlayer(contentsOf: url) 33 | self.player.play() 34 | self.playSoundInLoop() 35 | } 36 | catch { 37 | logger.error("Error to play audio = \(url)") 38 | } 39 | 40 | // Request the system broadcast 41 | logger.info("initializeScreenShare - requesting broadcast") 42 | AppDelegate.shared.clickScreenShareButton() 43 | 44 | let eventName = ReactNativeEventEmitter.EVENT.onBroadcastRequested.rawValue 45 | logger.info("initializeScreenShare - emitting event \(eventName)") 46 | ReactNativeEventEmitter.emitter.sendEvent(withName: eventName, body: nil); 47 | 48 | // Clear the current video frame, so the ScreenSharePublisher knows it's a new screenshare 49 | IPCCurrentVideoFrame.shared.clear() 50 | } 51 | 52 | // React native exposed method (called when user click the button to share screen) 53 | @objc func createScreenShareOffer(_ stunTurnJson:String) -> Void { 54 | logger.info("createScreenShareOffer \(stunTurnJson)") 55 | Task.init { 56 | let optionalSdp = await ScreenBroadcasterService.shared.createOffer() 57 | if(optionalSdp != nil){ 58 | let sdp = optionalSdp! 59 | self.logger.info("Got SDP back from screenBroadcaster: \(sdp)") 60 | 61 | ReactNativeEventEmitter.emitter.sendEvent(withName: ReactNativeEventEmitter.EVENT.onScreenShareOfferCreated.rawValue, body: sdp) 62 | } 63 | } 64 | } 65 | 66 | @objc func setScreenShareRemoteSDP(_ remoteSDP:String) -> Void { 67 | logger.info("setScreenShareRemoteSDP call arrived on swift: \(remoteSDP)") 68 | 69 | Task.init { 70 | let optionalSdp = await ScreenBroadcasterService.shared.setRemoteSDP(remoteSDP: remoteSDP) 71 | ReactNativeEventEmitter.emitter.sendEvent(withName: ReactNativeEventEmitter.EVENT.onSetScreenShareRemoteSDPCompleted.rawValue, body: nil) 72 | } 73 | 74 | } 75 | 76 | 77 | @objc func addScreenShareRemoteIceCandidate(_ remoteCandidate:String) -> Void { 78 | logger.info("addScreenShareRemoteIceCandidate call arrived on swift: \(remoteCandidate)") 79 | // Send request of "add remote ICE candidate" to broadcast upload extension 80 | // TIP - the handling of this method response is done in observer6 of BigBlueButtonSDK class 81 | logger.info("addScreenShareRemoteIceCandidate - persisting information on UserDefaults") 82 | // BBBSharedData 83 | // .getUserDefaults(appGroupName: BigBlueButtonSDK.getAppGroupName()) 84 | // .set(BBBSharedData.generatePayload(properties: [ 85 | // "candidate": remoteCandidate 86 | // ]), forKey: BBBSharedData.SharedData.addScreenShareRemoteIceCandidate) 87 | 88 | } 89 | 90 | @objc func stopScreenShareBroadcastExtension() -> Void { 91 | logger.info("stopScreenShareBroadcastExtension call arrived on swift") 92 | let userDefaults = UserDefaults(suiteName: "group.org.bigbluebutton.tablet") 93 | userDefaults?.set(true, forKey: "stopBroadcast") 94 | userDefaults?.synchronize() 95 | } 96 | 97 | func activeAudioSession(bool BoolToActive: Bool){ 98 | do{ 99 | try audioSession.setActive(BoolToActive) 100 | }catch{ 101 | logger.error("Error to change status of audioSession") 102 | } 103 | } 104 | 105 | //This method prevents the sound that keeps the app active in the background 106 | func playSoundInLoop(){ 107 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3000) { 108 | self.logger.info("restarting music") 109 | self.player.currentTime = 0.1; 110 | self.playSoundInLoop()//recursive call 111 | } 112 | 113 | } 114 | 115 | } 116 | 117 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import '@/i18n'; 2 | import { Picker } from '@react-native-picker/picker'; 3 | import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; 4 | import { useFonts } from 'expo-font'; 5 | import React from 'react'; 6 | import { useTranslation } from 'react-i18next'; 7 | import 'react-native-reanimated'; 8 | 9 | import { ThemedText } from '@/components/ThemedText'; 10 | import { ThemedView } from '@/components/ThemedView'; 11 | import { useColorScheme } from '@/hooks/useColorScheme'; 12 | import { Button, KeyboardAvoidingView, Platform, StyleSheet, TextInput, View } from 'react-native'; 13 | import MeetingWebView from './MeetingWebView'; 14 | 15 | export default function RootLayout() { 16 | const colorScheme = useColorScheme(); 17 | const [loaded] = useFonts({ 18 | SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), 19 | }); 20 | const { t, i18n } = useTranslation(); 21 | const [selectedLanguage, setSelectedLanguage] = React.useState(i18n.language); 22 | const [showMeeting, setShowMeeting] = React.useState(false); 23 | const [meetingUrl, setMeetingUrl] = React.useState('https://demo-ios-bbb30.bbb.imdt.dev/rooms/xy8-0jk-asw-v1f/join'); 24 | 25 | const handleLanguageChange = (lang: string) => { 26 | setSelectedLanguage(lang); 27 | i18n.changeLanguage(lang); 28 | }; 29 | 30 | if (!loaded) { 31 | // Async font loading only occurs in development. 32 | return null; 33 | } 34 | 35 | if (showMeeting) { 36 | return ( 37 | setShowMeeting(false)} 40 | /> 41 | ); 42 | } 43 | 44 | return ( 45 | 46 | 47 | 51 | 52 | {t('home.title')} 53 | 54 | 55 | {t('home.subtitle')} 56 | 57 | 58 | {t('home.description')} 59 | 60 | 61 | 62 | {t('home.inputLabel')} 63 | setShowMeeting(true)} 71 | /> 72 |