Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 15 additions & 31 deletions example/ios/LiveKit Broadcast Extension/SampleHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@ private enum Constants {
}

class SampleHandler: RPBroadcastSampleHandler {

private var clientConnection: SocketConnection?
private var uploader: SampleUploader?

private var frameCount: Int = 0

var socketFilePath: String {
let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupIdentifier)
return sharedContainer?.appendingPathComponent("rtc_SSFD").path ?? ""
}

override init() {
super.init()
if let connection = SocketConnection(filePath: socketFilePath) {
clientConnection = connection
setupConnection()

uploader = SampleUploader(connection: connection)
}
os_log(.debug, log: broadcastLogger, "%{public}s", socketFilePath)
Expand All @@ -40,28 +40,25 @@ class SampleHandler: RPBroadcastSampleHandler {
override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) {
// User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
frameCount = 0

DarwinNotificationCenter.shared.postNotification(.broadcastStarted)
openConnection()
startReplayKit()
}

override func broadcastPaused() {
// User has requested to pause the broadcast. Samples will stop being delivered.
}

override func broadcastResumed() {
// User has requested to resume the broadcast. Samples delivery will resume.
}

override func broadcastFinished() {
// User has requested to finish the broadcast.
DarwinNotificationCenter.shared.postNotification(.broadcastStopped)
clientConnection?.close()
closeReplayKit()

}

override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
switch sampleBufferType {
case RPSampleBufferType.video:
Expand All @@ -73,11 +70,11 @@ class SampleHandler: RPBroadcastSampleHandler {
}

private extension SampleHandler {

func setupConnection() {
clientConnection?.didClose = { [weak self] error in
os_log(.debug, log: broadcastLogger, "client connection did close \(String(describing: error))")

if let error = error {
self?.finishBroadcastWithError(error)
} else {
Expand All @@ -88,7 +85,7 @@ private extension SampleHandler {
}
}
}

func openConnection() {
let queue = DispatchQueue(label: "broadcast.connectTimer")
let timer = DispatchSource.makeTimerSource(queue: queue)
Expand All @@ -97,23 +94,10 @@ private extension SampleHandler {
guard self?.clientConnection?.open() == true else {
return
}

timer.cancel()
}

timer.resume()
}

func startReplayKit() {
let group=UserDefaults(suiteName: Constants.appGroupIdentifier)
group!.set(false, forKey: "closeReplayKitFromNative")
group!.set(false, forKey: "closeReplayKitFromFlutter")
group!.set(true, forKey: "hasSampleBroadcast")
}

func closeReplayKit() {
let group = UserDefaults(suiteName: Constants.appGroupIdentifier)
group!.set(true, forKey:"closeReplayKitFromNative")
group!.set(false, forKey: "hasSampleBroadcast")
}
}
55 changes: 1 addition & 54 deletions example/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,68 +3,15 @@ import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

var replayKitChannel: FlutterMethodChannel! = nil
var observeTimer: Timer?
var hasEmittedFirstSample = false;


override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
guard let controller = window?.rootViewController as? FlutterViewController else {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

replayKitChannel = FlutterMethodChannel(name: "io.livekit.example.flutter/replaykit-channel",binaryMessenger: controller.binaryMessenger)

replayKitChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
self.handleReplayKitFromFlutter(result: result, call:call)
})

GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

func handleReplayKitFromFlutter(result:FlutterResult, call: FlutterMethodCall){
switch (call.method) {
case "startReplayKit":
self.hasEmittedFirstSample = false
let group=UserDefaults(suiteName: "group.io.livekit.example.flutter")
group!.set(false, forKey: "closeReplayKitFromNative")
group!.set(false, forKey: "closeReplayKitFromFlutter")
self.observeReplayKitStateChanged()
break
case "closeReplayKit":
let group=UserDefaults(suiteName: "group.io.livekit.example.flutter")
group!.set(true,forKey: "closeReplayKitFromFlutter")
result(true)
break
default:
return result(FlutterMethodNotImplemented)
}
}

func observeReplayKitStateChanged(){
if (self.observeTimer != nil) {
return
}

let group=UserDefaults(suiteName: "group.io.livekit.example.flutter")
self.observeTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (timer) in
let closeReplayKitFromNative=group!.bool(forKey: "closeReplayKitFromNative")
let hasSampleBroadcast=group!.bool(forKey: "hasSampleBroadcast")

if (closeReplayKitFromNative) {
self.hasEmittedFirstSample = false
self.replayKitChannel.invokeMethod("closeReplayKitFromNative", arguments: true)
} else if (hasSampleBroadcast) {
if (!self.hasEmittedFirstSample) {
self.hasEmittedFirstSample = true
self.replayKitChannel.invokeMethod("hasSampleBroadcast", arguments: true)
}
}
}
}
}
42 changes: 0 additions & 42 deletions example/lib/method_channels/replay_kit_channel.dart

This file was deleted.

24 changes: 0 additions & 24 deletions example/lib/pages/room.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:livekit_example/method_channels/replay_kit_channel.dart';

import '../exts.dart';
import '../utils.dart';
Expand All @@ -30,7 +29,6 @@ class _RoomPageState extends State<RoomPage> {
List<ParticipantTrack> participantTracks = [];
EventsListener<RoomEvent> get _listener => widget.listener;
bool get fastConnection => widget.room.engine.fastConnectOptions != null;
bool _flagStartedReplayKit = false;
@override
void initState() {
super.initState();
Expand All @@ -49,10 +47,6 @@ class _RoomPageState extends State<RoomPage> {
Hardware.instance.setSpeakerphoneOn(true);
}

if (lkPlatformIs(PlatformType.iOS)) {
ReplayKitChannel.listenMethodChannel(widget.room);
}

if (lkPlatformIsDesktop()) {
onWindowShouldClose = () async {
unawaited(widget.room.disconnect());
Expand All @@ -66,9 +60,6 @@ class _RoomPageState extends State<RoomPage> {
void dispose() {
// always dispose listener
(() async {
if (lkPlatformIs(PlatformType.iOS)) {
ReplayKitChannel.closeReplayKit();
}
widget.room.removeListener(_onRoomDidUpdate);
await _listener.dispose();
await widget.room.dispose();
Expand Down Expand Up @@ -212,26 +203,11 @@ class _RoomPageState extends State<RoomPage> {
if (localParticipantTracks != null) {
for (var t in localParticipantTracks) {
if (t.isScreenShare) {
if (lkPlatformIs(PlatformType.iOS)) {
if (!_flagStartedReplayKit) {
_flagStartedReplayKit = true;

ReplayKitChannel.startReplayKit();
}
}
screenTracks.add(ParticipantTrack(
participant: widget.room.localParticipant!,
type: ParticipantTrackType.kScreenShare,
));
} else {
if (lkPlatformIs(PlatformType.iOS)) {
if (_flagStartedReplayKit) {
_flagStartedReplayKit = false;

ReplayKitChannel.closeReplayKit();
}
}

userMediaTracks.add(
ParticipantTrack(participant: widget.room.localParticipant!));
}
Expand Down
11 changes: 0 additions & 11 deletions example/lib/widgets/controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -190,23 +190,12 @@ class _ControlsWidgetState extends State<ControlsWidget> {

await requestBackgroundPermission();
}
if (lkPlatformIs(PlatformType.iOS)) {
var track = await LocalVideoTrack.createScreenShareTrack(
const ScreenShareCaptureOptions(
useiOSBroadcastExtension: true,
maxFrameRate: 15.0,
),
);
await participant.publishVideoTrack(track);
return;
}

if (lkPlatformIsWebMobile()) {
await context
.showErrorDialog('Screen share is not supported on mobile web');
return;
}

await participant.setScreenShareEnabled(true, captureScreenAudio: true);
}

Expand Down
23 changes: 23 additions & 0 deletions ios/Classes/BroadcastBundleInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2025 LiveKit
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/// Bundle info related to the broadcast extension.
enum BroadcastBundleInfo {

/// Bundle identifier of the broadcast extension.
@BundleInfo("RTCScreenSharingExtension")
static var screenSharingExtension: String?
}
51 changes: 51 additions & 0 deletions ios/Classes/BroadcastManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2025 LiveKit
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Combine
import ReplayKit

@available(iOS 13.0, *)
final class BroadcastManager {

static let shared = BroadcastManager()

let isBroadcastingPublisher: AnyPublisher<Bool, Never> =
Publishers.Merge(
DarwinNotificationCenter.shared.publisher(for: .broadcastStarted).map { _ in true },
DarwinNotificationCenter.shared.publisher(for: .broadcastStopped).map { _ in false }
)
.eraseToAnyPublisher()

func requestActivation() {
guard let bundleIdentifier = BroadcastBundleInfo.screenSharingExtension else { return }
Task { await Self.showPicker(for: bundleIdentifier) }
}

func requestStop() {
DarwinNotificationCenter.shared.postNotification(.broadcastRequestStop)
}

/// Convenience function to show broadcast extension picker.
@MainActor private static func showPicker(for preferredExtension: String) {
let view = RPSystemBroadcastPickerView()
view.preferredExtension = preferredExtension
view.showsMicrophoneButton = false

let selector = NSSelectorFromString("buttonPressed:")
guard view.responds(to: selector) else { return }
view.perform(selector, with: nil)
}
}
Loading