diff --git a/StikJIT/JSSupport/PiPController.swift b/StikJIT/JSSupport/PiPController.swift new file mode 100644 index 0000000..ef0ba81 --- /dev/null +++ b/StikJIT/JSSupport/PiPController.swift @@ -0,0 +1,55 @@ +// +// PiPController.swift +// StikDebug +// +// Created by Stossy11 on 10/07/2025. +// + + +import SwiftUI +import AVKit +import AVFoundation + +struct VideoPlayerView: UIViewRepresentable { + let pipController: PiPController = PiPController.shared + let view: any View + + func makeUIView(context: Context) -> UIView { + let containerView = UIView() + + // Setup player layer + let playerLayer = AVPlayerLayer() + playerLayer.frame = CGRect(x: 0, y: 0, width: 200, height: 150) + + // Load video from bundle + guard let videoURL = Bundle.main.url(forResource: "black", withExtension: "MP4") else { + print("Could not find black.MP4 in bundle") + return containerView + } + + let asset = AVAsset(url: videoURL) + let playerItem = AVPlayerItem(asset: asset) + let player = AVPlayer(playerItem: playerItem) + + playerLayer.player = player + player.isMuted = true + player.allowsExternalPlayback = true + + containerView.layer.addSublayer(playerLayer) + + // Convert SwiftUI view to UIView using UIHostingController + let hostingController = UIHostingController(rootView: AnyView(self.view)) + let swiftUIAsUIView = hostingController.view! + + // Set the converted UIView to the PiPController + pipController.customUIView = swiftUIAsUIView + + pipController.setupPiP(with: playerLayer) + + return containerView + } + + func updateUIView(_ uiView: UIView, context: Context) { + // Update UI if needed + } +} diff --git a/StikJIT/JSSupport/RunJSView.swift b/StikJIT/JSSupport/RunJSView.swift index bb260c5..efc1c05 100644 --- a/StikJIT/JSSupport/RunJSView.swift +++ b/StikJIT/JSSupport/RunJSView.swift @@ -69,15 +69,40 @@ class RunJSViewModel: ObservableObject { if let semaphore { semaphore.signal() } + PiPController.shared.stopPiP() + DispatchQueue.main.async { if let exception = self.context?.exception { self.logs.append(exception.debugDescription) } - self.logs.append("Script Execution Completed.") + + self.logs.append("Script Execution Completed, \nYou are safe to close the PIP Window.") } } } +struct RunJSViewPiP: View { + @Binding var model: RunJSViewModel? + @State var logs: [String] = [] + let timer = Timer.publish(every: 0.034, on: .main, in: .common).autoconnect() + + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + ForEach(logs.suffix(6).indices, id: \.self) { index in + Text(logs.suffix(6)[index]) + .font(.system(size: 12)) + .foregroundStyle(.white) + } + } + .padding() + .onReceive(timer) { _ in + self.logs = model?.logs ?? [] + } + } +} + + struct RunJSView: View { @ObservedObject var model: RunJSViewModel diff --git a/StikJIT/StikJIT-Bridging-Header.h b/StikJIT/StikJIT-Bridging-Header.h index c4ea98d..7320cf8 100644 --- a/StikJIT/StikJIT-Bridging-Header.h +++ b/StikJIT/StikJIT-Bridging-Header.h @@ -6,3 +6,4 @@ #include "idevice/idevice.h" #include "idevice/heartbeat.h" #include "JSSupport/JSSupport.h" +#include "Utilities/PiP/PiPController.h" diff --git a/StikJIT/Utilities/PiP/PiPController.h b/StikJIT/Utilities/PiP/PiPController.h new file mode 100644 index 0000000..7952f4d --- /dev/null +++ b/StikJIT/Utilities/PiP/PiPController.h @@ -0,0 +1,29 @@ +// +// PiPController.h +// StikDebug +// +// Created by Stossy11 on 10/07/2025. +// + + +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PiPController : NSObject + +@property (nonatomic, assign) BOOL isPiPActive; +@property (nonatomic, strong, nullable) UIView *customUIView; +@property (class, nonatomic, readonly) PiPController *shared; + +- (void)setupPiPWithPlayerLayer:(AVPlayerLayer *)playerLayer; +- (void)startPiP; +- (void)stopPiP; +- (void)togglePiP; + +@end + +NS_ASSUME_NONNULL_END diff --git a/StikJIT/Utilities/PiP/PiPController.m b/StikJIT/Utilities/PiP/PiPController.m new file mode 100644 index 0000000..83e939e --- /dev/null +++ b/StikJIT/Utilities/PiP/PiPController.m @@ -0,0 +1,138 @@ +// +// PiPController.m +// StikDebug +// +// Created by Stossy11 on 10/07/2025. +// + +#import "PiPController.h" + +@interface PiPController () +@property (nonatomic, strong, nullable) AVPictureInPictureController *pipController; +@property (nonatomic, strong, nullable) AVPlayerLayer *playerLayer; +@property (nonatomic, strong, nullable) UIView *customView; +@end + +@implementation PiPController + ++ (PiPController *)shared { + static PiPController *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _isPiPActive = NO; + [self setupAudioSession]; + } + return self; +} + +- (void)setupAudioSession { + NSError *error = nil; + AVAudioSession *audioSession = [AVAudioSession sharedInstance]; + + if (![audioSession setCategory:AVAudioSessionCategoryPlayback error:&error]) { + NSLog(@"Audio session setup error: %@", error.localizedDescription); + return; + } + + if (![audioSession setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error]) { + NSLog(@"Audio session setup error: %@", error.localizedDescription); + } +} + +- (void)setupPiPWithPlayerLayer:(AVPlayerLayer *)playerLayer { + if (![AVPictureInPictureController isPictureInPictureSupported]) { + NSLog(@"Picture in Picture not supported"); + return; + } + + self.playerLayer = playerLayer; + + self.pipController = [[AVPictureInPictureController alloc] initWithPlayerLayer:playerLayer]; + self.pipController.delegate = self; + + // Set controls style (equivalent to setValue:forKey: in Swift) + if ([self.pipController respondsToSelector:@selector(setControlsStyle:)]) { + [self.pipController setValue:@(1) forKey:@"controlsStyle"]; + } +} + +- (void)startPiP { + if (self.pipController.isPictureInPictureActive) return; + + [self.pipController startPictureInPicture]; +} + +- (void)stopPiP { + if (!self.pipController.isPictureInPictureActive) return; + + [self.pipController stopPictureInPicture]; +} + +- (void)togglePiP { + if (self.isPiPActive) { + [self stopPiP]; + } else { + [self startPiP]; + } +} + +#pragma mark - AVPictureInPictureControllerDelegate + +- (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { + if (!self.customUIView) { + NSLog(@"Fatal error: customUIView = nil"); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + self.isPiPActive = YES; + }); + + self.customView = self.customUIView; + + UIWindow *window = [UIApplication sharedApplication].windows.firstObject; + if (window && self.customView) { + self.customView.backgroundColor = [UIColor clearColor]; + [window addSubview:self.customView]; + + self.customView.translatesAutoresizingMaskIntoConstraints = NO; + [NSLayoutConstraint activateConstraints:@[ + [self.customView.topAnchor constraintEqualToAnchor:window.topAnchor], + [self.customView.leadingAnchor constraintEqualToAnchor:window.leadingAnchor], + [self.customView.trailingAnchor constraintEqualToAnchor:window.trailingAnchor], + [self.customView.bottomAnchor constraintEqualToAnchor:window.bottomAnchor] + ]]; + } +} + + +- (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { + NSLog(@"PiP started"); +} + +- (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { + dispatch_async(dispatch_get_main_queue(), ^{ + self.isPiPActive = NO; + }); + + // Remove custom view + [self.customView removeFromSuperview]; + self.customView = nil; +} + +- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController failedToStartPictureInPictureWithError:(NSError *)error { + NSLog(@"Failed to start PiP: %@", error.localizedDescription); + dispatch_async(dispatch_get_main_queue(), ^{ + self.isPiPActive = NO; + }); +} + +@end diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index 808bb42..e062d0c 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -59,6 +59,19 @@ struct HomeView: View { Color(colorScheme == .dark ? .black : .white) .edgesIgnoringSafeArea(.all) + if useDefaultScript { + VideoPlayerView(view: RunJSViewPiP(model: $jsModel)) + .frame(width: 200, height: 150) + .background(Color.clear) + .cornerRadius(10) + .onAppear() { + Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { _ in + PiPController.shared.startPiP() + } + } + .overlay(colorScheme == .dark ? .black : .white) + } + VStack(spacing: 25) { Spacer() VStack(spacing: 5) { @@ -224,6 +237,12 @@ struct HomeView: View { .onReceive(timer) { _ in refreshBackground() checkPairingFileExists() + + if useDefaultScript { + PiPController.shared.startPiP() + } else { + PiPController.shared.stopPiP() + } } .fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, .propertyList]) {result in switch result { @@ -315,10 +334,7 @@ struct HomeView: View { .navigationTitle(selectedScript) .navigationBarTitleDisplayMode(.inline) } - - } - } .onChange(of: scriptViewShow) { oldValue, newValue in if !newValue, let jsModel { diff --git a/StikJIT/black.MP4 b/StikJIT/black.MP4 new file mode 100644 index 0000000..a9b3959 Binary files /dev/null and b/StikJIT/black.MP4 differ