├── 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 |
74 |
75 |
76 |
77 | {/* Language Picker */}
78 | {/* Removed from here */}
79 |
80 | {/* Language Picker moved here for bottom-left alignment */}
81 |
82 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | );
95 | }
96 |
97 | const styles = StyleSheet.create({
98 | container: {
99 | flex: 1,
100 | justifyContent: 'center',
101 | alignItems: 'center',
102 | padding: 24,
103 | },
104 | inner: {
105 | width: '100%',
106 | maxWidth: 420,
107 | alignItems: 'center',
108 | },
109 | title: {
110 | marginBottom: 8,
111 | textAlign: 'center',
112 | },
113 | subtitle: {
114 | marginBottom: 8,
115 | textAlign: 'center',
116 | },
117 | description: {
118 | marginBottom: 24,
119 | textAlign: 'center',
120 | color: '#687076',
121 | },
122 | card: {
123 | width: '100%',
124 | backgroundColor: 'rgba(0,0,0,0.04)',
125 | borderRadius: 16,
126 | padding: 20,
127 | marginBottom: 24,
128 | shadowColor: '#000',
129 | shadowOpacity: 0.08,
130 | shadowRadius: 8,
131 | shadowOffset: { width: 0, height: 2 },
132 | elevation: 2,
133 | },
134 | inputLabel: {
135 | marginBottom: 8,
136 | fontWeight: '600',
137 | },
138 | input: {
139 | height: 44,
140 | borderColor: '#ccc',
141 | borderWidth: 1,
142 | borderRadius: 8,
143 | paddingHorizontal: 12,
144 | marginBottom: 12,
145 | backgroundColor: '#fff',
146 | fontSize: 16,
147 | },
148 | spacer: {
149 | height: 24,
150 | },
151 | languagePickerContainer: {
152 | position: 'absolute',
153 | left: 24,
154 | bottom: 24,
155 | width: 270,
156 | backgroundColor: 'rgba(255,255,255,0.9)',
157 | borderRadius: 8,
158 | padding: 4,
159 | // Optional: add shadow for visibility
160 | shadowColor: '#000',
161 | shadowOpacity: 0.08,
162 | shadowRadius: 8,
163 | shadowOffset: { width: 0, height: 2 },
164 | elevation: 2,
165 | },
166 | });
167 |
--------------------------------------------------------------------------------
/ios/bigbluebuttontablet/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Expo
2 | import React
3 | import ReactAppDependencyProvider
4 | import WebKit
5 | import AVFoundation
6 | import ReplayKit
7 |
8 | /// Main app delegate handling app lifecycle, Expo + React Native setup,
9 | /// screen sharing initialization, and heartbeat signaling.
10 | @UIApplicationMain
11 | public class AppDelegate: ExpoAppDelegate {
12 |
13 | // MARK: - Properties
14 |
15 | var window: UIWindow?
16 | private var pickerView: RPSystemBroadcastPickerView?
17 | var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
18 | var reactNativeFactory: RCTReactNativeFactory?
19 |
20 | /// Shared instance for global access.
21 | static var shared: AppDelegate {
22 | return UIApplication.shared.delegate as! AppDelegate
23 | }
24 |
25 | // MARK: - Application Lifecycle
26 |
27 | /// Main entry point after app launch.
28 | public override func application(
29 | _ application: UIApplication,
30 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
31 | ) -> Bool {
32 |
33 | // React Native bootstrapping
34 | let delegate = ReactNativeDelegate()
35 | let factory = ExpoReactNativeFactory(delegate: delegate)
36 | delegate.dependencyProvider = RCTAppDependencyProvider()
37 |
38 | reactNativeDelegate = delegate
39 | reactNativeFactory = factory
40 | bindReactNativeFactory(factory)
41 |
42 | #if os(iOS) || os(tvOS)
43 | // Set up UIWindow and attach root React Native view
44 | window = UIWindow(frame: UIScreen.main.bounds)
45 | factory.startReactNative(
46 | withModuleName: "main",
47 | in: window,
48 | launchOptions: launchOptions
49 | )
50 | #endif
51 |
52 | // Start frame polling logic for screen broadcasting
53 | ScreenSharePublisher.start()
54 |
55 | // Set up hidden system broadcast picker
56 | setupScreenShareButton()
57 |
58 | // Start heartbeat for companion processes
59 | startHeartbeat()
60 |
61 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
62 | }
63 |
64 | // MARK: - Deep Linking Support
65 |
66 | /// Handles links like myapp://somepath
67 | public override func application(
68 | _ app: UIApplication,
69 | open url: URL,
70 | options: [UIApplication.OpenURLOptionsKey: Any] = [:]
71 | ) -> Bool {
72 | return super.application(app, open: url, options: options) ||
73 | RCTLinkingManager.application(app, open: url, options: options)
74 | }
75 |
76 | /// Handles universal links (e.g. website -> app)
77 | public override func application(
78 | _ application: UIApplication,
79 | continue userActivity: NSUserActivity,
80 | restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
81 | ) -> Bool {
82 | let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
83 | return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
84 | }
85 |
86 | // MARK: - Screen Share Picker Setup
87 |
88 | /// Initializes the hidden screen sharing button to trigger ReplayKit broadcast.
89 | private func setupScreenShareButton() {
90 | DispatchQueue.main.async {
91 | if self.pickerView == nil {
92 | let picker = RPSystemBroadcastPickerView(
93 | frame: CGRect(x: -1000, y: -1000, width: 50, height: 50)
94 | )
95 | picker.preferredExtension = "org.bigbluebutton.tablet.BigBlueButton-Screen-Share"
96 | picker.showsMicrophoneButton = false
97 |
98 | if let keyWindow = UIApplication.shared
99 | .connectedScenes
100 | .compactMap({ $0 as? UIWindowScene })
101 | .flatMap({ $0.windows })
102 | .first(where: { $0.isKeyWindow }) {
103 | keyWindow.addSubview(picker)
104 | self.pickerView = picker
105 | } else {
106 | print("Could not find key window")
107 | }
108 | }
109 | }
110 | }
111 |
112 | /// Programmatically triggers the screen share broadcast picker button.
113 | public func clickScreenShareButton() {
114 | DispatchQueue.main.async {
115 | if let button = self.pickerView?.subviews.first(where: { $0 is UIButton }) as? UIButton {
116 | button.sendActions(for: .touchUpInside)
117 | } else {
118 | print("Broadcast picker button not found")
119 | }
120 | }
121 | }
122 |
123 | // MARK: - Heartbeat
124 |
125 | /// Continuously updates a shared UserDefaults timestamp every second
126 | /// so external services (like the broadcast extension) know the app is alive.
127 | func startHeartbeat() {
128 | Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
129 | let defaults = UserDefaults(suiteName: "group.org.bigbluebutton.tablet")!
130 | defaults.set(Date().timeIntervalSince1970, forKey: "mainAppHeartBeat")
131 | }
132 | }
133 | }
134 |
135 | // MARK: - React Native Bridge Delegate
136 |
137 | /// Provides the correct JS bundle URL for development and production modes.
138 | class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
139 |
140 | /// Returns the JS bundle URL for the React bridge.
141 | override func sourceURL(for bridge: RCTBridge) -> URL? {
142 | return bridge.bundleURL ?? bundleURL()
143 | }
144 |
145 | /// Fallback for the JS bundle URL if not using the dev client.
146 | override func bundleURL() -> URL? {
147 | #if DEBUG
148 | return RCTBundleURLProvider.sharedSettings().jsBundleURL(
149 | forBundleRoot: ".expo/.virtual-metro-entry"
150 | )
151 | #else
152 | return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
153 | #endif
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/ios/InterProcessCommunication/IPCFileManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IPCFileManager.swift
3 | // bigbluebuttontablet
4 | //
5 | // Provides shared memory-mapped file access for communication between the
6 | // Broadcast extension and the main application.
7 | //
8 | // Created by Tiago Daniel Jacobs, 2025
9 |
10 | import Foundation
11 |
12 | // MARK: - Low-level POSIX Memory-Mapping Manager
13 |
14 | /// Handles memory-mapped file I/O using POSIX system calls.
15 | /// Each operation maps the file into memory, performs the read/write,
16 | /// and then safely unmaps and closes the file.
17 | public final class IPCFileManager {
18 |
19 | public static let shared = IPCFileManager()
20 | private init() {}
21 |
22 | // MARK: - Public API
23 |
24 | /// Writes the given `data` to the specified `offset` in a memory-mapped file.
25 | /// If the file does not exist, it is created and sized appropriately.
26 | ///
27 | /// - Returns: `false` if the input is invalid or any file I/O error occurs.
28 | @discardableResult
29 | public func write(_ data: Data,
30 | to path: String,
31 | size: Int,
32 | at offset: Int = 0) -> Bool {
33 | guard offset >= 0, offset + data.count <= size else { return false }
34 |
35 | guard let (fd, _, mem) = mapFile(path: path, size: size), let mem = mem else {
36 | return false
37 | }
38 |
39 | data.withUnsafeBytes { src in
40 | memcpy(mem.advanced(by: offset), src.baseAddress!, data.count)
41 | }
42 |
43 | msync(mem, size, MS_SYNC) // Flush changes to disk
44 | unmapFile(fd: fd, mem: mem, size: size)
45 | return true
46 | }
47 |
48 | /// Reads `count` bytes from the file starting at `offset`.
49 | ///
50 | /// - Returns: `Data` if successful, or `nil` on failure.
51 | public func read(from path: String,
52 | size: Int,
53 | count: Int,
54 | offset: Int = 0) -> Data? {
55 | guard offset >= 0, offset + count <= size else { return nil }
56 |
57 | guard let (fd, _, mem) = mapFile(path: path, size: size), let mem = mem else {
58 | return nil
59 | }
60 |
61 | let bytes = UnsafeRawPointer(mem.advanced(by: offset))
62 | let data = Data(bytes: bytes, count: count)
63 |
64 | unmapFile(fd: fd, mem: mem, size: size)
65 | return data
66 | }
67 |
68 | /// Clears the entire file by zero-filling it.
69 | @discardableResult
70 | public func clear(path: String, size: Int) -> Bool {
71 | guard let (fd, _, mem) = mapFile(path: path, size: size), let mem = mem else {
72 | return false
73 | }
74 |
75 | memset(mem, 0, size)
76 | msync(mem, size, MS_SYNC)
77 | unmapFile(fd: fd, mem: mem, size: size)
78 | return true
79 | }
80 |
81 | /// Checks whether the first 3 bytes of the mapped file are zero.
82 | /// Used to determine whether the file represents a “clean” frame.
83 | public func isClean(path: String, size: Int) -> Bool {
84 | guard let (fd, _, mem) = mapFile(path: path, size: size), let mem = mem else {
85 | return false
86 | }
87 |
88 | let b0 = mem.load(fromByteOffset: 0, as: UInt8.self)
89 | let b1 = mem.load(fromByteOffset: 1, as: UInt8.self)
90 | let b2 = mem.load(fromByteOffset: 2, as: UInt8.self)
91 |
92 | unmapFile(fd: fd, mem: mem, size: size)
93 | return b0 == 0 && b1 == 0 && b2 == 0
94 | }
95 |
96 | // MARK: - Internal Mapping Utilities
97 |
98 | /// Maps a file into memory using mmap().
99 | /// - Returns: file descriptor, size, and mapped memory pointer.
100 | private func mapFile(path: String,
101 | size: Int,
102 | options: Int32 = O_RDWR | O_CREAT)
103 | -> (fd: Int32, sz: Int, mem: UnsafeMutableRawPointer?)? {
104 |
105 | let fd = open(path, options, 0o666) // rw-rw-rw-
106 | guard fd >= 0 else { return nil }
107 |
108 | if ftruncate(fd, off_t(size)) != 0 {
109 | close(fd)
110 | return nil
111 | }
112 |
113 | let mem = mmap(nil, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)
114 | guard mem != MAP_FAILED else {
115 | close(fd)
116 | return nil
117 | }
118 |
119 | return (fd, size, mem)
120 | }
121 |
122 | /// Unmaps and closes a memory-mapped file safely.
123 | private func unmapFile(fd: Int32,
124 | mem: UnsafeMutableRawPointer,
125 | size: Int) {
126 | munmap(mem, size)
127 | close(fd)
128 | }
129 | }
130 |
131 | // MARK: - High-Level Shared Memory Manager
132 |
133 | /// Provides safe, high-level access to a memory-mapped file representing
134 | /// the current screen-share frame. Wraps IPCFileManager internally.
135 | public final class IPCCurrentVideoFrame {
136 |
137 | public static let shared = IPCCurrentVideoFrame()
138 | private init() {}
139 |
140 | // MARK: - Shared File Configuration
141 |
142 | private static let appGroupID = "group.org.bigbluebutton.tablet"
143 | private static let fileSize = 20 * 1024 * 1024 // 20 MB max buffer
144 |
145 | /// Absolute file path for the memory-mapped frame file.
146 | private static var filePath: String = {
147 | guard let url = FileManager.default
148 | .containerURL(forSecurityApplicationGroupIdentifier: appGroupID) else {
149 | fatalError("App Group container not found")
150 | }
151 | return url.appendingPathComponent("currentFrame.mmap").path
152 | }()
153 |
154 | // MARK: - Public Frame API
155 |
156 | /// Writes data to shared memory at a given offset.
157 | @discardableResult
158 | public func set(_ data: Data, at offset: Int = 0) -> Bool {
159 | return IPCFileManager.shared.write(
160 | data,
161 | to: Self.filePath,
162 | size: Self.fileSize,
163 | at: offset
164 | )
165 | }
166 |
167 | /// Reads a specified number of bytes from shared memory.
168 | /// Defaults to full file size if count == 0.
169 | public func get(count: Int = 0, from offset: Int = 0) -> Data? {
170 | return IPCFileManager.shared.read(
171 | from: Self.filePath,
172 | size: Self.fileSize,
173 | count: count == 0 ? Self.fileSize : count,
174 | offset: offset
175 | )
176 | }
177 |
178 | /// Zeroes out the shared memory, marking it as clean/unused.
179 | @discardableResult
180 | public func clear() -> Bool {
181 | return IPCFileManager.shared.clear(
182 | path: Self.filePath,
183 | size: Self.fileSize
184 | )
185 | }
186 |
187 | /// Returns true if the shared memory contains no pixel data.
188 | public func isClean() -> Bool {
189 | return IPCFileManager.shared.isClean(
190 | path: Self.filePath,
191 | size: Self.fileSize
192 | )
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/ios/bigbluebuttontablet/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"idiom":"watch","filename":"172.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"86x86","expected-size":"172","role":"quickLook"},{"idiom":"watch","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"40x40","expected-size":"80","role":"appLauncher"},{"idiom":"watch","filename":"88.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"40mm","scale":"2x","size":"44x44","expected-size":"88","role":"appLauncher"},{"idiom":"watch","filename":"102.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"45mm","scale":"2x","size":"51x51","expected-size":"102","role":"appLauncher"},{"idiom":"watch","filename":"108.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"49mm","scale":"2x","size":"54x54","expected-size":"108","role":"appLauncher"},{"idiom":"watch","filename":"92.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"41mm","scale":"2x","size":"46x46","expected-size":"92","role":"appLauncher"},{"idiom":"watch","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"50x50","expected-size":"100","role":"appLauncher"},{"idiom":"watch","filename":"196.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"98x98","expected-size":"196","role":"quickLook"},{"idiom":"watch","filename":"216.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"108x108","expected-size":"216","role":"quickLook"},{"idiom":"watch","filename":"234.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"45mm","scale":"2x","size":"117x117","expected-size":"234","role":"quickLook"},{"idiom":"watch","filename":"258.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"49mm","scale":"2x","size":"129x129","expected-size":"258","role":"quickLook"},{"idiom":"watch","filename":"48.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"24x24","expected-size":"48","role":"notificationCenter"},{"idiom":"watch","filename":"55.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"27.5x27.5","expected-size":"55","role":"notificationCenter"},{"idiom":"watch","filename":"66.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"45mm","scale":"2x","size":"33x33","expected-size":"66","role":"notificationCenter"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"3x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"2x"},{"size":"1024x1024","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch-marketing","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]}
--------------------------------------------------------------------------------
/ios/ScreenSharing/PixelBufferSerialization.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PixelBufferSerialization.swift
3 | // bigbluebuttontablet
4 | //
5 | // Created by Tiago Daniel Jacobs, 2025
6 | //
7 |
8 | import Foundation
9 | import CoreVideo
10 | import CoreMedia
11 | import ImageIO // for CGImagePropertyOrientation
12 |
13 | // MARK: - Constants
14 |
15 | /// ASCII prefix used as a sanity check before decoding pixel buffer payloads.
16 | private let kPrefix = Data("BBB".utf8) // 3 bytes
17 |
18 | // MARK: - Header Definition
19 |
20 | /// Describes the binary layout of the serialized pixel buffer header.
21 | /// Total header size: 28 bytes + 8 (timestamp) = 36 bytes.
22 | public struct PixelHeader {
23 | static let size = 7 * MemoryLayout.size + MemoryLayout.size
24 |
25 | var timestampNs: Int64
26 | var width: UInt32
27 | var height: UInt32
28 | var pixelFormat: UInt32
29 | var bytesPerRow: UInt32
30 | var dataSize: UInt32
31 | var orientation: UInt32
32 | var cookie: UInt32
33 |
34 | /// Full initializer for manual construction.
35 | init(
36 | timestampNs: Int64,
37 | width: UInt32,
38 | height: UInt32,
39 | pixelFormat: UInt32,
40 | bytesPerRow: UInt32,
41 | dataSize: UInt32,
42 | orientation: UInt32,
43 | cookie: UInt32
44 | ) {
45 | self.timestampNs = timestampNs
46 | self.width = width
47 | self.height = height
48 | self.pixelFormat = pixelFormat
49 | self.bytesPerRow = bytesPerRow
50 | self.dataSize = dataSize
51 | self.orientation = orientation
52 | self.cookie = cookie
53 | }
54 |
55 | /// Initializes header from a live pixel buffer (only supports single-plane).
56 | init(
57 | timestampNs: Int64,
58 | buffer: CVPixelBuffer,
59 | orientation: CGImagePropertyOrientation,
60 | cookie: UInt32 = .random(in: 1...9000)
61 | ) {
62 | let pixelBytes = UInt32(CVPixelBufferGetDataSize(buffer))
63 | self.init(
64 | timestampNs: timestampNs,
65 | width: UInt32(CVPixelBufferGetWidth(buffer)),
66 | height: UInt32(CVPixelBufferGetHeight(buffer)),
67 | pixelFormat: CVPixelBufferGetPixelFormatType(buffer),
68 | bytesPerRow: UInt32(CVPixelBufferGetBytesPerRow(buffer)),
69 | dataSize: pixelBytes,
70 | orientation: UInt32(orientation.rawValue),
71 | cookie: cookie
72 | )
73 | }
74 |
75 | /// Encodes the struct into raw Data using native-endian layout.
76 | func encode() -> Data {
77 | var tmp = self
78 | return Data(bytes: &tmp, count: PixelHeader.size)
79 | }
80 |
81 | /// Decodes a header from Data. Returns nil if data is insufficient.
82 | static func decode(from data: Data) -> PixelHeader? {
83 | guard data.count >= PixelHeader.size else { return nil }
84 | return data.withUnsafeBytes { rawPtr -> PixelHeader in
85 | var hdr = PixelHeader(
86 | timestampNs: 0,
87 | width: 0,
88 | height: 0,
89 | pixelFormat: 0,
90 | bytesPerRow: 0,
91 | dataSize: 0,
92 | orientation: 0,
93 | cookie: 0
94 | )
95 | memcpy(&hdr, rawPtr.baseAddress!, PixelHeader.size)
96 | return hdr
97 | }
98 | }
99 | }
100 |
101 | // MARK: - Deserialization Errors
102 |
103 | /// Errors thrown when decoding a pixel buffer fails due to malformed layout or mismatch.
104 | enum PBDeserializationError: Error {
105 | case prefixMissing
106 | case headerTooShort
107 | case invalidHeader
108 | case inconsistentStride
109 | case sizeMismatch(expected: Int, got: Int)
110 | case cookieMismatch(expected: UInt32, got: UInt32)
111 | case pixelBufferCreateFailed
112 | case baseAddressUnavailable
113 | }
114 |
115 | // MARK: - Serialization
116 |
117 | /// Serializes a pixel buffer (single-plane only) to binary format.
118 | /// Layout:
119 | /// - 1) 3-byte prefix `"BBB"`
120 | /// - 2) 36-byte header (PixelHeader)
121 | /// - 3) Raw pixel bytes
122 | /// - 4) 4-byte trailer with repeated cookie
123 | func serializePixelBufferFull(
124 | pixelBuffer: CVPixelBuffer,
125 | orientation: CGImagePropertyOrientation = .up,
126 | timestampNs: Int64
127 | ) -> Data? {
128 | CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
129 | defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }
130 |
131 | guard let basePtr = CVPixelBufferGetBaseAddress(pixelBuffer) else { return nil }
132 |
133 | let pixelBytes = CVPixelBufferGetDataSize(pixelBuffer)
134 | let header = PixelHeader(
135 | timestampNs: timestampNs,
136 | buffer: pixelBuffer,
137 | orientation: orientation
138 | )
139 | let headerData = header.encode()
140 |
141 | var out = Data(capacity: kPrefix.count + headerData.count + pixelBytes + 4)
142 | out.append(kPrefix) // Step 1: Prefix
143 | out.append(headerData) // Step 2: Header
144 | out.append(basePtr.assumingMemoryBound(to: UInt8.self), count: pixelBytes) // Step 3: Pixel bytes
145 |
146 | var trailerLE = header.cookie.littleEndian
147 | out.append(Data(bytes: &trailerLE, count: 4)) // Step 4: Trailer
148 |
149 | return out
150 | }
151 |
152 | // MARK: - Deserialization
153 |
154 | /// Reverses `serializePixelBufferFull`, reconstructing the pixel buffer from data.
155 | /// Ensures integrity using prefix, data length, and cookie checks.
156 | func deserializePixelBufferFull(
157 | _ data: Data
158 | ) throws -> (
159 | buffer: CVPixelBuffer,
160 | orientation: CGImagePropertyOrientation,
161 | header: PixelHeader
162 | ) {
163 | // 0. Verify prefix
164 | guard data.starts(with: kPrefix) else {
165 | throw PBDeserializationError.prefixMissing
166 | }
167 |
168 | let headerStart = kPrefix.count
169 | let headerEnd = headerStart + PixelHeader.size
170 | guard data.count >= headerEnd else {
171 | throw PBDeserializationError.headerTooShort
172 | }
173 |
174 | // 1. Decode header
175 | let headerData = data.subdata(in: headerStart.. Int in
191 | data.copyBytes(to: dst, from: trailerOffset..<(trailerOffset + 4))
192 | }
193 | trailerCookie = UInt32(littleEndian: trailerCookie)
194 | guard trailerCookie == header.cookie else {
195 | throw PBDeserializationError.cookieMismatch(expected: header.cookie, got: trailerCookie)
196 | }
197 |
198 | // 3. Create pixel buffer
199 | var pbOpt: CVPixelBuffer?
200 | let attrs: CFDictionary = [
201 | kCVPixelBufferCGImageCompatibilityKey: true,
202 | kCVPixelBufferCGBitmapContextCompatibilityKey: true
203 | ] as CFDictionary
204 |
205 | let status = CVPixelBufferCreate(
206 | kCFAllocatorDefault,
207 | Int(header.width),
208 | Int(header.height),
209 | header.pixelFormat,
210 | attrs,
211 | &pbOpt
212 | )
213 | guard status == kCVReturnSuccess, let pixelBuffer = pbOpt else {
214 | throw PBDeserializationError.pixelBufferCreateFailed
215 | }
216 |
217 | // 4. Copy pixel bytes
218 | CVPixelBufferLockBaseAddress(pixelBuffer, [])
219 | defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, []) }
220 |
221 | guard let destPtr = CVPixelBufferGetBaseAddress(pixelBuffer) else {
222 | throw PBDeserializationError.baseAddressUnavailable
223 | }
224 |
225 | let pixelRange = headerEnd..
134 | // Split option: 'foo,bar' -> ['foo', 'bar']
135 | def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
136 | // Trim all elements in place.
137 | for (i in 0.. 0) {
142 | println "android.packagingOptions.$prop += $options ($options.length)"
143 | // Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
144 | options.each {
145 | android.packagingOptions[prop] += it
146 | }
147 | }
148 | }
149 |
150 | dependencies {
151 | // The version of react-native is set by the React Native Gradle Plugin
152 | implementation("com.facebook.react:react-android")
153 |
154 | def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
155 | def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
156 | def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
157 |
158 | if (isGifEnabled) {
159 | // For animated gif support
160 | implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}")
161 | }
162 |
163 | if (isWebpEnabled) {
164 | // For webp support
165 | implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}")
166 | if (isWebpAnimatedEnabled) {
167 | // Animated webp support
168 | implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}")
169 | }
170 | }
171 |
172 | if (hermesEnabled.toBoolean()) {
173 | implementation("com.facebook.react:hermes-android")
174 | } else {
175 | implementation jscFlavor
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/app/MeetingWebView.tsx:
--------------------------------------------------------------------------------
1 | import { Ionicons } from '@expo/vector-icons';
2 | import React, { useRef, useState } from 'react';
3 | import { Clipboard, Dimensions, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
4 | import { WebView, WebViewMessageEvent } from 'react-native-webview';
5 | import { AppLogger } from '../components/AppLogger';
6 | import DebugPopup from '../components/DebugPopup';
7 | import stopScreenShare from './methods/stopScreenShare';
8 | import { handleWebviewMessage } from './webview/message-handler';
9 |
10 | interface MeetingWebViewProps {
11 | url: string;
12 | onClose: () => void;
13 | }
14 |
15 | export default function MeetingWebView({ url, onClose: externalOnClose }: MeetingWebViewProps) {
16 | const webviewRef = useRef(null);
17 | const [status, setStatus] = useState<'Loading' | 'Loaded' | 'Error'>('Loading');
18 | const [webLogs, _setWebLogs] = useState([]);
19 | const [appLogs, _setAppLogs] = useState(AppLogger.getInstance().getLogs());
20 |
21 | const onClose = React.useCallback(() => {
22 | stopScreenShare(1);
23 | externalOnClose();
24 | }, [externalOnClose]);
25 |
26 | // Robust popup state management
27 | const window = Dimensions.get('window');
28 | const initialPopupState = {
29 | visible: false,
30 | position: { x: window.width * 0.05, y: window.height * 0.15 },
31 | maximized: false,
32 | dragOffset: undefined as { x: number; y: number } | undefined,
33 | };
34 | type DebugPopupState = typeof initialPopupState;
35 | const [_debugPopupState, _setDebugPopupState] = useState(initialPopupState);
36 |
37 | // Logging wrappers for state setters
38 | const setDebugPopupState = (updater: DebugPopupState | ((prev: DebugPopupState) => DebugPopupState)) => {
39 | _setDebugPopupState((prev: DebugPopupState) => {
40 | const next = typeof updater === 'function' ? (updater as (prev: DebugPopupState) => DebugPopupState)(prev) : updater;
41 | return next;
42 | });
43 | };
44 | const setWebLogs = (updater: any) => {
45 | _setWebLogs((prev: any) => {
46 | const next = typeof updater === 'function' ? updater(prev) : updater;
47 | return next;
48 | });
49 | };
50 | const setAppLogs = (updater: any) => {
51 | _setAppLogs((prev: any) => {
52 | const next = typeof updater === 'function' ? updater(prev) : updater;
53 | return next;
54 | });
55 | };
56 | const setStatusLogged = (updater: any) => {
57 | setStatus((prev: any) => {
58 | const next = typeof updater === 'function' ? updater(prev) : updater;
59 | return next;
60 | });
61 | };
62 |
63 | // Subscribe to AppLogger updates
64 | React.useEffect(() => {
65 | const unsub = AppLogger.getInstance().subscribe(setAppLogs);
66 | return unsub;
67 | }, []);
68 |
69 | React.useEffect(() => {
70 | }, []);
71 |
72 | // --- Injected JS for WebView ---
73 | const injectedJS = `
74 | (function() {
75 | function wrapConsole(method) {
76 | var orig = console[method];
77 | console[method] = function() {
78 | var msg = Array.prototype.slice.call(arguments).join(' ');
79 | window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'web-log', level: method, msg }));
80 | orig.apply(console, arguments);
81 | };
82 | }
83 | ['log','info','warn','error'].forEach(wrapConsole);
84 | })();
85 | true;
86 | `;
87 |
88 | // --- WebView Message Handler ---
89 | function onWebviewMessage(event: WebViewMessageEvent) {
90 | try {
91 | const data = JSON.parse(event.nativeEvent.data);
92 |
93 | if (data.type === 'web-log') {
94 | setWebLogs((prev: any) => [
95 | `[${data.level.toUpperCase()}] ${new Date().toISOString()} ${data.msg}`,
96 | ...prev,
97 | ].slice(0, 500));
98 | } else {
99 | // Log all messages to app logger
100 | AppLogger.getInstance().info(`WebView postMessage: ${JSON.stringify(data)}`);
101 |
102 | // Call the actual message handler
103 | handleWebviewMessage(1, webviewRef, event);
104 | }
105 | } catch (e) {
106 | // Log non-JSON messages to app logger as well
107 | AppLogger.getInstance().info(`WebView postMessage (non-JSON): ${event.nativeEvent.data}`);
108 | }
109 | }
110 |
111 | // Popup handlers
112 | const handleOpenDebug = () => {
113 | setDebugPopupState({
114 | visible: true,
115 | position: { x: window.width * 0.05, y: window.height * 0.15 },
116 | maximized: false,
117 | dragOffset: undefined,
118 | });
119 | };
120 | const handleCloseDebug = () => setDebugPopupState((prev: DebugPopupState) => ({ ...prev, visible: false, dragOffset: undefined }));
121 | const handleMaximize = () => setDebugPopupState((prev: DebugPopupState) => ({ ...prev, maximized: true, dragOffset: undefined }));
122 | const handleRestore = () => setDebugPopupState((prev: DebugPopupState) => ({ ...prev, maximized: false, dragOffset: undefined }));
123 | const handleCopyApp = () => {
124 | if (typeof navigator !== 'undefined' && navigator.clipboard) {
125 | navigator.clipboard.writeText(appLogs.join('\n'));
126 | } else if (Clipboard) {
127 | Clipboard.setString(appLogs.join('\n'));
128 | }
129 | };
130 | const handleCopyWeb = () => {
131 | if (typeof navigator !== 'undefined' && navigator.clipboard) {
132 | navigator.clipboard.writeText(webLogs.join('\n'));
133 | } else if (Clipboard) {
134 | Clipboard.setString(webLogs.join('\n'));
135 | }
136 | };
137 | const handleClearApp = () => AppLogger.getInstance().clear();
138 | const handleClearWeb = () => setWebLogs([]);
139 |
140 | const handleReload = () => {
141 | // @ts-ignore
142 | webviewRef.current?.reload();
143 | setStatusLogged('Loading');
144 | };
145 |
146 | // Use the logging wrappers for state
147 | const debugPopupState = _debugPopupState;
148 |
149 | return (
150 |
151 | {/* Toolbar */}
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 | {status}
162 |
163 |
164 |
165 | {/* WebView */}
166 | setStatusLogged('Loading')}
170 | onLoadEnd={() => setStatusLogged('Loaded')}
171 | onError={() => setStatusLogged('Error')}
172 | onMessage={onWebviewMessage}
173 | style={styles.webview}
174 | contentMode='mobile'
175 | allowsInlineMediaPlayback={true}
176 | mediaCapturePermissionGrantType='grant'
177 | applicationNameForUserAgent='BigBlueButton-Tablet'
178 | injectedJavaScript={injectedJS}
179 | mediaPlaybackRequiresUserAction={false}
180 | webviewDebuggingEnabled={true}
181 | />
182 | {debugPopupState.visible && (
183 |
197 | )}
198 |
199 | );
200 | }
201 |
202 | const styles = StyleSheet.create({
203 | container: {
204 | flex: 1,
205 | backgroundColor: '#f8f9fb',
206 | },
207 | toolbar: {
208 | flexDirection: 'row',
209 | alignItems: 'center',
210 | paddingHorizontal: 16,
211 | paddingTop: Platform.OS === 'ios' ? 36 : 16, // extra top padding for iPad/iOS status bar
212 | paddingBottom: 12,
213 | backgroundColor: '#fff',
214 | borderBottomWidth: 1,
215 | borderBottomColor: '#e5e7eb',
216 | shadowColor: '#000',
217 | shadowOpacity: 0.06,
218 | shadowRadius: 8,
219 | shadowOffset: { width: 0, height: 2 },
220 | elevation: 3,
221 | zIndex: 2,
222 | },
223 | toolbarButton: {
224 | marginRight: 16,
225 | padding: 8,
226 | borderRadius: 6,
227 | backgroundColor: '#f1f3f6',
228 | justifyContent: 'center',
229 | alignItems: 'center',
230 | },
231 | statusContainer: {
232 | flex: 1,
233 | alignItems: 'flex-end',
234 | },
235 | statusText: {
236 | fontSize: 15,
237 | color: '#4b5563',
238 | fontWeight: '500',
239 | letterSpacing: 0.2,
240 | },
241 | webview: {
242 | flex: 1,
243 | },
244 | });
--------------------------------------------------------------------------------
/ios/ScreenSharing/ScreenShareService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScreenBroadcaster.swift
3 | //
4 | // Created by Tiago Daniel Jacobs, 2025
5 | //
6 |
7 | import os
8 | import WebRTC
9 | import UIKit
10 |
11 | /// Service responsible for capturing and broadcasting screen share frames using WebRTC.
12 | /// Implemented as a singleton.
13 | open class ScreenBroadcasterService {
14 |
15 | // MARK: - Singleton
16 |
17 | /// Shared instance of the broadcaster.
18 | public static let shared = ScreenBroadcasterService()
19 |
20 | // MARK: - Properties
21 |
22 | /// Logger for internal diagnostics.
23 | private var logger = os.Logger(subsystem: "BigBlueButtonTabletSDK", category: "ScreenBroadcasterService")
24 |
25 | /// The internal WebRTC client for media negotiation and frame pushing.
26 | private var webRTCClient: ScreenShareWebRTCClient
27 |
28 | /// App Group identifier (used for shared container storage, if needed).
29 | private var appGroupName: String = "group.org.bigbluebutton.tablet"
30 |
31 | /// JSON encoder used for serializing signaling data.
32 | private let encoder = JSONEncoder()
33 |
34 | /// Flag indicating whether the WebRTC connection has been successfully established.
35 | public var isConnected: Bool = false
36 |
37 | // MARK: - Initialization
38 |
39 | /// Initializes the service with default STUN servers and sets itself as the WebRTC delegate.
40 | private init() {
41 | self.webRTCClient = ScreenShareWebRTCClient(iceServers: [
42 | "stun:stun.l.google.com:19302",
43 | "stun:stun1.l.google.com:19302",
44 | "stun:stun2.l.google.com:19302",
45 | "stun:stun3.l.google.com:19302",
46 | "stun:stun4.l.google.com:19302"
47 | ])
48 | self.webRTCClient.delegate = self
49 | }
50 |
51 | private func reInit() {
52 | self.webRTCClient = ScreenShareWebRTCClient(iceServers: [
53 | "stun:stun.l.google.com:19302",
54 | "stun:stun1.l.google.com:19302",
55 | "stun:stun2.l.google.com:19302",
56 | "stun:stun3.l.google.com:19302",
57 | "stun:stun4.l.google.com:19302"
58 | ])
59 | self.webRTCClient.delegate = self
60 | }
61 |
62 | // MARK: - WebRTC Signaling Methods
63 |
64 | /// Creates an SDP offer to initiate screen-sharing connection.
65 | public func createOffer() async -> String? {
66 | do {
67 | let rtcSessionDescription = try await self.webRTCClient.offer()
68 | return rtcSessionDescription.sdp
69 | } catch {
70 | logger.error("Error on webRTCClient.offer")
71 | return nil
72 | }
73 | }
74 |
75 | /// Sets the remote SDP (received from the server).
76 | public func setRemoteSDP(remoteSDP: String) async -> Bool {
77 | do {
78 | try await self.webRTCClient.setRemoteSDP(remoteSDP: remoteSDP)
79 | return true
80 | } catch {
81 | return false
82 | }
83 | }
84 |
85 | /// Adds a remote ICE candidate received from the server.
86 | public func addRemoteCandidate(remoteCandidate: IceCandidate) async -> Bool {
87 | do {
88 | try await self.webRTCClient.setRemoteCandidate(remoteIceCandidate: remoteCandidate)
89 | return true
90 | } catch {
91 | return false
92 | }
93 | }
94 |
95 | // MARK: - Video Frame Pushing
96 |
97 | /// Pushes a screen-captured video frame to the WebRTC stream.
98 | public func pushVideoFrame(
99 | timeStampNs: Int64,
100 | orientation: CGImagePropertyOrientation,
101 | imageBuffer: CVImageBuffer
102 | ) {
103 | // Ensure we are connected before pushing frames
104 | guard isConnected else {
105 | logger.info("Ignoring pushVideoFrame - not connected")
106 | return
107 | }
108 |
109 | // Determine proper rotation for the frame based on image orientation
110 | var rotationFrame: RTCVideoRotation = ._0
111 | switch orientation.rawValue {
112 | case 6: rotationFrame = ._270 // Right
113 | case 8: rotationFrame = ._90 // Left
114 | default: break
115 | }
116 |
117 | // Wrap the image buffer in a WebRTC pixel buffer
118 | let rtcPixlBuffer = RTCCVPixelBuffer(pixelBuffer: imageBuffer)
119 |
120 | // Set ratio if it's not already defined
121 | if !webRTCClient.getIsRatioDefined() {
122 | webRTCClient.setRatio(originalWidth: rtcPixlBuffer.width, originalHeight: rtcPixlBuffer.height)
123 | }
124 |
125 | // Create a video frame and push it to the WebRTC client
126 | let rtcVideoFrame = RTCVideoFrame(
127 | buffer: rtcPixlBuffer,
128 | rotation: rotationFrame,
129 | timeStampNs: timeStampNs
130 | )
131 | webRTCClient.push(videoFrame: rtcVideoFrame)
132 | }
133 | }
134 |
135 | // MARK: - WebRTC Delegate Implementation
136 |
137 | extension ScreenBroadcasterService: ScreenShareWebRTCClientDelegate {
138 |
139 | /// Called when a new local ICE candidate is discovered.
140 | public func webRTCClient(_ client: ScreenShareWebRTCClient, didDiscoverLocalCandidate rtcIceCandidate: RTCIceCandidate) {
141 | do {
142 | let iceCandidate = IceCandidate(from: rtcIceCandidate)
143 | let iceCandidateAsJsonData = try encoder.encode(iceCandidate)
144 | let iceCandidateAsJsonString = String(decoding: iceCandidateAsJsonData, as: UTF8.self)
145 |
146 | // Uncomment and use if shared data needs to be communicated to another process
147 | /*
148 | BBBSharedData
149 | .getUserDefaults(appGroupName: self.appGroupName)
150 | .set(BBBSharedData.generatePayload(properties: [
151 | "iceJson": iceCandidateAsJsonString
152 | ]), forKey: BBBSharedData.SharedData.onScreenShareLocalIceCandidate)
153 | */
154 | } catch {
155 | logger.error("Error handling ICE candidate")
156 | }
157 | }
158 |
159 | /// Called when ICE connection state changes.
160 | public func webRTCClient(_ client: ScreenShareWebRTCClient, didChangeIceConnectionState state: RTCIceConnectionState) {
161 | switch state {
162 | case .connected:
163 | logger.info("didChangeConnectionState -> connected")
164 | case .completed:
165 | logger.info("didChangeConnectionState -> completed")
166 | case .disconnected, .failed, .closed:
167 | logger.info("didChangeConnectionState -> \(state.rawValue) - cleaning up")
168 | isConnected = false
169 | disconnect()
170 | reInit()
171 | default:
172 | break
173 | }
174 | }
175 |
176 | public func disconnect() {
177 | logger.info("Cleaning up ScreenBroadcasterService...")
178 | isConnected = false
179 | webRTCClient.close()
180 | }
181 |
182 | /// Called when ICE gathering state changes.
183 | public func webRTCClient(_ client: ScreenShareWebRTCClient, didChangeIceGatheringState state: RTCIceGatheringState) {
184 | switch state {
185 | case .new:
186 | logger.info("didChangeGatheringState -> new")
187 | case .gathering:
188 | logger.info("didChangeGatheringState -> gathering")
189 | case .complete:
190 | logger.info("didChangeGatheringState -> complete")
191 | @unknown default:
192 | logger.error("Unknown gathering state: \(state.rawValue)")
193 | }
194 | }
195 |
196 | /// Called when signaling state changes.
197 | public func webRTCClient(_ client: ScreenShareWebRTCClient, didChangeSignalingState state: RTCSignalingState) {
198 | var stateString = ""
199 |
200 | switch state {
201 | case .haveLocalOffer:
202 | logger.info("peerConnection new signaling state -> haveLocalOffer")
203 | stateString = "have-local-offer"
204 | case .haveLocalPrAnswer:
205 | logger.info("peerConnection new signaling state -> haveLocalPrAnswer")
206 | stateString = "have-local-pranswer"
207 | case .haveRemoteOffer:
208 | logger.info("peerConnection new signaling state -> haveRemoteOffer")
209 | stateString = "have-remote-offer"
210 | case .haveRemotePrAnswer:
211 | logger.info("peerConnection new signaling state -> haveRemotePrAnswer")
212 | stateString = "have-remote-pranswer"
213 | case .stable:
214 | logger.info("peerConnection new signaling state -> stable")
215 | stateString = "stable"
216 | case .closed:
217 | logger.info("peerConnection new signaling state -> closed")
218 | stateString = "closed"
219 | default:
220 | logger.error("peerConnection new signaling state -> UNKNOWN")
221 | }
222 |
223 | // Connection considered active after first signaling state change
224 | isConnected = true
225 |
226 | // Uncomment if shared data needs to be updated externally
227 | /*
228 | BBBSharedData
229 | .getUserDefaults(appGroupName: self.appGroupName)
230 | .set(BBBSharedData.generatePayload(properties: [
231 | "newState": stateString
232 | ]), forKey: BBBSharedData.SharedData.onScreenShareSignalingStateChange)
233 | */
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | if ! command -v java >/dev/null 2>&1
137 | then
138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
139 |
140 | Please set the JAVA_HOME variable in your environment to match the
141 | location of your Java installation."
142 | fi
143 | fi
144 |
145 | # Increase the maximum file descriptors if we can.
146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
147 | case $MAX_FD in #(
148 | max*)
149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
150 | # shellcheck disable=SC2039,SC3045
151 | MAX_FD=$( ulimit -H -n ) ||
152 | warn "Could not query maximum file descriptor limit"
153 | esac
154 | case $MAX_FD in #(
155 | '' | soft) :;; #(
156 | *)
157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
158 | # shellcheck disable=SC2039,SC3045
159 | ulimit -n "$MAX_FD" ||
160 | warn "Could not set maximum file descriptor limit to $MAX_FD"
161 | esac
162 | fi
163 |
164 | # Collect all arguments for the java command, stacking in reverse order:
165 | # * args from the command line
166 | # * the main class name
167 | # * -classpath
168 | # * -D...appname settings
169 | # * --module-path (only if needed)
170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
171 |
172 | # For Cygwin or MSYS, switch paths to Windows format before running java
173 | if "$cygwin" || "$msys" ; then
174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
176 |
177 | JAVACMD=$( cygpath --unix "$JAVACMD" )
178 |
179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
180 | for arg do
181 | if
182 | case $arg in #(
183 | -*) false ;; # don't mess with options #(
184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
185 | [ -e "$t" ] ;; #(
186 | *) false ;;
187 | esac
188 | then
189 | arg=$( cygpath --path --ignore --mixed "$arg" )
190 | fi
191 | # Roll the args list around exactly as many times as the number of
192 | # args, so each arg winds up back in the position where it started, but
193 | # possibly modified.
194 | #
195 | # NB: a `for` loop captures its iteration list before it begins, so
196 | # changing the positional parameters here affects neither the number of
197 | # iterations, nor the values presented in `arg`.
198 | shift # remove old arg
199 | set -- "$@" "$arg" # push replacement arg
200 | done
201 | fi
202 |
203 |
204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
206 |
207 | # Collect all arguments for the java command:
208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
209 | # and any embedded shellness will be escaped.
210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
211 | # treated as '${Hostname}' itself on the command line.
212 |
213 | set -- \
214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
215 | -classpath "$CLASSPATH" \
216 | org.gradle.wrapper.GradleWrapperMain \
217 | "$@"
218 |
219 | # Stop when "xargs" is not available.
220 | if ! command -v xargs >/dev/null 2>&1
221 | then
222 | die "xargs is not available"
223 | fi
224 |
225 | # Use "xargs" to parse quoted args.
226 | #
227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
228 | #
229 | # In Bash we could simply go:
230 | #
231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
232 | # set -- "${ARGS[@]}" "$@"
233 | #
234 | # but POSIX shell has neither arrays nor command substitution, so instead we
235 | # post-process each arg (as a line of input to sed) to backslash-escape any
236 | # character that might be a shell metacharacter, then use eval to reverse
237 | # that process (while maintaining the separation between arguments), and wrap
238 | # the whole thing up as a single "set" statement.
239 | #
240 | # This will of course break if any of these variables contains a newline or
241 | # an unmatched quote.
242 | #
243 |
244 | eval "set -- $(
245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
246 | xargs -n1 |
247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
248 | tr '\n' ' '
249 | )" '"$@"'
250 |
251 | exec "$JAVACMD" "$@"
252 |
--------------------------------------------------------------------------------
/ios/ScreenSharing/WebRTC/ScreenShareWebRTCClient.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScreenShareWebRTCClient.swift
3 | // WebRTC
4 | // Created by Tiago Daniel Jacobs, 2025
5 | //
6 |
7 | import Foundation
8 | import WebRTC
9 | import os
10 |
11 | // MARK: - Delegate Protocol
12 |
13 | /// Delegate to receive WebRTC events for signaling, ICE, and connection lifecycle.
14 | public protocol ScreenShareWebRTCClientDelegate: AnyObject {
15 | func webRTCClient(_ client: ScreenShareWebRTCClient, didDiscoverLocalCandidate candidate: RTCIceCandidate)
16 | func webRTCClient(_ client: ScreenShareWebRTCClient, didChangeIceConnectionState state: RTCIceConnectionState)
17 | func webRTCClient(_ client: ScreenShareWebRTCClient, didChangeIceGatheringState state: RTCIceGatheringState)
18 | func webRTCClient(_ client: ScreenShareWebRTCClient, didChangeSignalingState state: RTCSignalingState)
19 | }
20 |
21 | // MARK: - WebRTC Client Class
22 |
23 | /// Manages WebRTC peer connection, video track setup, and signaling for screen share publishing.
24 | open class ScreenShareWebRTCClient: NSObject {
25 |
26 | private var logger = os.Logger(subsystem: "BigBlueButtonTabletSDK", category: "WebRTCClient")
27 |
28 | /// Shared PeerConnectionFactory used to create tracks and peer connections.
29 | private static let factory: RTCPeerConnectionFactory = {
30 | RTCInitializeSSL()
31 | let encoderFactory = RTCDefaultVideoEncoderFactory()
32 | let decoderFactory = RTCDefaultVideoDecoderFactory()
33 | encoderFactory.preferredCodec = RTCVideoCodecInfo(name: kRTCVideoCodecVp8Name)
34 | return RTCPeerConnectionFactory(encoderFactory: encoderFactory, decoderFactory: decoderFactory)
35 | }()
36 |
37 | // MARK: - Properties
38 |
39 | public weak var delegate: ScreenShareWebRTCClientDelegate?
40 | private let peerConnection: RTCPeerConnection
41 | private let rtcAudioSession = RTCAudioSession.sharedInstance()
42 | private let audioQueue = DispatchQueue(label: "audio")
43 | private let mediaConstrains = [
44 | kRTCMediaConstraintsOfferToReceiveAudio: kRTCMediaConstraintsValueFalse,
45 | kRTCMediaConstraintsOfferToReceiveVideo: kRTCMediaConstraintsValueFalse
46 | ]
47 |
48 | private var videoSource: RTCVideoSource?
49 | private var videoCapturer: RTCVideoCapturer?
50 | private var localVideoTrack: RTCVideoTrack?
51 | private var isRatioDefined = false
52 |
53 | // MARK: - Initializer
54 |
55 | /// Prevent direct use. Always use designated init with iceServers.
56 | @available(*, unavailable)
57 | override init() {
58 | fatalError("init is unavailable. Use init(iceServers:) instead.")
59 | }
60 |
61 | /// Constructs the client and sets up the peer connection with STUN config.
62 | public required init(iceServers: [String]) {
63 | let config = RTCConfiguration()
64 | config.iceServers = [RTCIceServer(urlStrings: iceServers)]
65 | config.sdpSemantics = .unifiedPlan
66 | config.continualGatheringPolicy = .gatherOnce
67 |
68 | let constraints = RTCMediaConstraints(
69 | mandatoryConstraints: nil,
70 | optionalConstraints: ["DtlsSrtpKeyAgreement": kRTCMediaConstraintsValueTrue]
71 | )
72 |
73 | guard let pc = Self.factory.peerConnection(with: config, constraints: constraints, delegate: nil) else {
74 | fatalError("Failed to create RTCPeerConnection")
75 | }
76 |
77 | self.peerConnection = pc
78 | super.init()
79 |
80 | createMediaSenders()
81 | self.peerConnection.delegate = self
82 | }
83 |
84 | // MARK: - Signaling
85 |
86 | /// Generates a WebRTC offer and sets it as the local SDP.
87 | public func offer() async throws -> RTCSessionDescription {
88 | let constraints = RTCMediaConstraints(mandatoryConstraints: mediaConstrains, optionalConstraints: nil)
89 | let sdp = try await peerConnection.offer(for: constraints)
90 | try await peerConnection.setLocalDescription(sdp)
91 | return sdp
92 | }
93 |
94 | /// Sets the received remote SDP answer.
95 | public func setRemoteSDP(remoteSDP: String) async throws {
96 | let desc = RTCSessionDescription(type: .answer, sdp: remoteSDP)
97 | try await peerConnection.setRemoteDescription(desc)
98 | }
99 |
100 | /// Adds a remote ICE candidate to the peer connection.
101 | public func setRemoteCandidate(remoteIceCandidate: IceCandidate) async throws {
102 | let rtcCandidate = RTCIceCandidate(
103 | sdp: remoteIceCandidate.candidate,
104 | sdpMLineIndex: remoteIceCandidate.sdpMLineIndex,
105 | sdpMid: remoteIceCandidate.sdpMid
106 | )
107 | try await peerConnection.add(rtcCandidate)
108 | }
109 |
110 | /// Adds a remote ICE candidate with a completion handler (legacy fallback).
111 | func set(remoteCandidate: RTCIceCandidate, completion: @escaping (Error?) -> Void) {
112 | peerConnection.add(remoteCandidate, completionHandler: completion)
113 | }
114 |
115 | // MARK: - Video Frame Ingestion
116 |
117 | /// Pushes a raw `RTCVideoFrame` into the video capturer pipeline.
118 | public func push(videoFrame: RTCVideoFrame) {
119 | guard let source = videoSource, let capturer = videoCapturer else { return }
120 | source.capturer(capturer, didCapture: videoFrame)
121 | // print("RTCVideoFrame pushed to server.")
122 | }
123 |
124 | // MARK: - Media Setup
125 |
126 | /// Creates and registers a video track as a media sender.
127 | private func createMediaSenders() {
128 | let streamId = "stream"
129 | let videoTrack = createVideoTrack()
130 | self.localVideoTrack = videoTrack
131 | peerConnection.add(videoTrack, streamIds: [streamId])
132 | }
133 |
134 | /// Instantiates a screen-cast video track and binds it to the capturer.
135 | private func createVideoTrack() -> RTCVideoTrack {
136 | videoSource = Self.factory.videoSource(forScreenCast: true)
137 | videoCapturer = RTCVideoCapturer(delegate: videoSource!)
138 | let track = Self.factory.videoTrack(with: videoSource!, trackId: "video0")
139 | track.isEnabled = true
140 | return track
141 | }
142 |
143 | // MARK: - Resolution Configuration
144 |
145 | /// Sets the output format (width/height/fps) for the video source.
146 | public func setRatio(originalWidth: Int32, originalHeight: Int32) {
147 | videoSource?.adaptOutputFormat(toWidth: originalWidth, height: originalHeight, fps: 30)
148 | isRatioDefined = true
149 | }
150 |
151 | /// Returns whether the capture resolution has been explicitly defined.
152 | public func getIsRatioDefined() -> Bool {
153 | return isRatioDefined
154 | }
155 |
156 | public func close() {
157 | // Always run teardown on main (or signaling) thread to avoid races with callbacks.
158 | DispatchQueue.main.async {
159 | self.logger.info("Closing WebRTC peer connection (start)")
160 |
161 | // Prevent delegate callbacks to your owner while we tear down
162 | self.delegate = nil
163 |
164 | // Disable local track so no further frames are pushed
165 | self.localVideoTrack?.isEnabled = false
166 |
167 | // Remove all senders (clean removal of tracks from PC)
168 | for sender in self.peerConnection.senders {
169 | // removeTrack returns Bool (best-effort)
170 | _ = self.peerConnection.removeTrack(sender)
171 | }
172 |
173 | // Clear the peer connection delegate so no callbacks into this instance
174 | self.peerConnection.delegate = nil
175 |
176 | // Close the peer connection (final)
177 | self.peerConnection.close()
178 |
179 | // Release video resources
180 | self.videoSource = nil
181 | self.videoCapturer = nil
182 | self.localVideoTrack = nil
183 | self.isRatioDefined = false
184 |
185 | self.logger.info("Closing WebRTC peer connection (done)")
186 | }
187 | }
188 |
189 |
190 | }
191 |
192 | // MARK: - RTCPeerConnectionDelegate
193 |
194 | extension ScreenShareWebRTCClient: RTCPeerConnectionDelegate {
195 |
196 | public func peerConnection(_ pc: RTCPeerConnection, didChange state: RTCSignalingState) {
197 | logger.info("Signaling state changed: \(state.rawValue)")
198 | delegate?.webRTCClient(self, didChangeSignalingState: state)
199 | }
200 |
201 | public func peerConnection(_ pc: RTCPeerConnection, didChange newState: RTCIceConnectionState) {
202 | logger.info("ICE connection state: \(newState.rawValue)")
203 | delegate?.webRTCClient(self, didChangeIceConnectionState: newState)
204 | }
205 |
206 | public func peerConnection(_ pc: RTCPeerConnection, didChange newState: RTCIceGatheringState) {
207 | logger.info("ICE gathering state: \(newState.rawValue)")
208 | delegate?.webRTCClient(self, didChangeIceGatheringState: newState)
209 | }
210 |
211 | public func peerConnection(_ pc: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) {
212 | logger.info("Discovered new ICE candidate")
213 | delegate?.webRTCClient(self, didDiscoverLocalCandidate: candidate)
214 | }
215 |
216 | public func peerConnection(_ pc: RTCPeerConnection, didAdd stream: RTCMediaStream) {
217 | logger.info("Added media stream: \(stream.streamId)")
218 | }
219 |
220 | public func peerConnection(_ pc: RTCPeerConnection, didRemove stream: RTCMediaStream) {
221 | logger.info("Removed media stream: \(stream.streamId)")
222 | }
223 |
224 | public func peerConnectionShouldNegotiate(_ pc: RTCPeerConnection) {
225 | logger.info("Negotiation needed")
226 | }
227 |
228 | public func peerConnection(_ pc: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) {
229 | logger.info("Removed ICE candidates")
230 | }
231 |
232 | public func peerConnection(_ pc: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) {
233 | logger.info("Opened data channel")
234 | }
235 | }
236 |
237 | // MARK: - Track Enable/Disable Utilities
238 |
239 | extension ScreenShareWebRTCClient {
240 | private func setTrackEnabled(_ type: T.Type, isEnabled: Bool) {
241 | peerConnection.transceivers
242 | .compactMap { $0.sender.track as? T }
243 | .forEach { $0.isEnabled = isEnabled }
244 | }
245 | }
246 |
247 | // MARK: - Video Track Controls
248 |
249 | extension ScreenShareWebRTCClient {
250 | func hideVideo() { setVideoEnabled(false) }
251 | func showVideo() { setVideoEnabled(true) }
252 |
253 | private func setVideoEnabled(_ isEnabled: Bool) {
254 | setTrackEnabled(RTCVideoTrack.self, isEnabled: isEnabled)
255 | }
256 | }
257 |
258 | // MARK: - Audio Track Controls
259 |
260 | extension ScreenShareWebRTCClient {
261 | func muteAudio() { setAudioEnabled(false) }
262 | func unmuteAudio() { setAudioEnabled(true) }
263 |
264 | private func setAudioEnabled(_ isEnabled: Bool) {
265 | setTrackEnabled(RTCAudioTrack.self, isEnabled: isEnabled)
266 | }
267 |
268 | func speakerOff() {
269 | // Stub: implement audio routing override if needed
270 | }
271 |
272 | func speakerOn() {
273 | // Stub: implement audio routing override if needed
274 | }
275 | }
276 |
277 | // MARK: - RTCDataChannelDelegate
278 |
279 | extension ScreenShareWebRTCClient: RTCDataChannelDelegate {
280 | public func dataChannelDidChangeState(_ channel: RTCDataChannel) {
281 | debugPrint("DataChannel state changed: \(channel.readyState)")
282 | }
283 |
284 | public func dataChannel(_ channel: RTCDataChannel, didReceiveMessageWith buffer: RTCDataBuffer) {
285 | debugPrint("DataChannel received message: \(buffer)")
286 | }
287 | }
288 |
--------------------------------------------------------------------------------