mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 04:20:30 +00:00
Add PIP to Script Logging (#219)
* Add PIP Stuff * Add background video
This commit is contained in:
parent
77128558ee
commit
3919d5e43c
7 changed files with 268 additions and 4 deletions
55
StikJIT/JSSupport/PiPController.swift
Normal file
55
StikJIT/JSSupport/PiPController.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -6,3 +6,4 @@
|
|||
#include "idevice/idevice.h"
|
||||
#include "idevice/heartbeat.h"
|
||||
#include "JSSupport/JSSupport.h"
|
||||
#include "Utilities/PiP/PiPController.h"
|
||||
|
|
|
|||
29
StikJIT/Utilities/PiP/PiPController.h
Normal file
29
StikJIT/Utilities/PiP/PiPController.h
Normal 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
|
||||
138
StikJIT/Utilities/PiP/PiPController.m
Normal file
138
StikJIT/Utilities/PiP/PiPController.m
Normal 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
|
||||
|
|
@ -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
BIN
StikJIT/black.MP4
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue