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
55 changes: 55 additions & 0 deletions StikJIT/JSSupport/PiPController.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
27 changes: 26 additions & 1 deletion StikJIT/JSSupport/RunJSView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions StikJIT/StikJIT-Bridging-Header.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
#include "idevice/idevice.h"
#include "idevice/heartbeat.h"
#include "JSSupport/JSSupport.h"
#include "Utilities/PiP/PiPController.h"
29 changes: 29 additions & 0 deletions StikJIT/Utilities/PiP/PiPController.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// PiPController.h
// StikDebug
//
// Created by Stossy11 on 10/07/2025.
//


#import <Foundation/Foundation.h>
#import <AVKit/AVKit.h>
#import <AVFoundation/AVFoundation.h>
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface PiPController : NSObject <AVPictureInPictureControllerDelegate>

@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
138 changes: 138 additions & 0 deletions StikJIT/Utilities/PiP/PiPController.m
Original file line number Diff line number Diff line change
@@ -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
22 changes: 19 additions & 3 deletions StikJIT/Views/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -315,10 +334,7 @@ struct HomeView: View {
.navigationTitle(selectedScript)
.navigationBarTitleDisplayMode(.inline)
}


}

}
.onChange(of: scriptViewShow) { oldValue, newValue in
if !newValue, let jsModel {
Expand Down
Binary file added StikJIT/black.MP4
Binary file not shown.