Add PIP to Script Logging (#219)

* Add PIP Stuff

* Add background video
This commit is contained in:
Stossy11 2025-07-10 22:46:02 +10:00 • committed by GitHub
parent 77128558ee
commit 3919d5e43c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 268 additions and 4 deletions

View file

@ -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
}
}

View file

@ -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

View file

@ -6,3 +6,4 @@
#include "idevice/idevice.h"
#include "idevice/heartbeat.h"
#include "JSSupport/JSSupport.h"
#include "Utilities/PiP/PiPController.h"

View file

@ -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

View file

@ -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

View file

@ -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 {

BIN
StikJIT/black.MP4 Normal file

Binary file not shown.