Better PiP (#221)

This commit is contained in:
Huge_Black 2025-07-11 22:11:20 +08:00 • committed by GitHub
parent 3e079cffc3
commit 41b280f7c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 27 additions and 246 deletions

View file

@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
17C744F02E20BED000834F17 /* Pipify in Frameworks */ = {isa = PBXBuildFile; productRef = 17C744EF2E20BED000834F17 /* Pipify */; };
68D569BE2E1B415700A5BA36 /* CodeEditorView in Frameworks */ = {isa = PBXBuildFile; productRef = 68D569BD2E1B415700A5BA36 /* CodeEditorView */; };
68D569C02E1B415700A5BA36 /* LanguageSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 68D569BF2E1B415700A5BA36 /* LanguageSupport */; };
DC139F6E2DE97EA400F63846 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC139F6D2DE97EA400F63846 /* WidgetKit.framework */; };
@ -149,6 +150,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
17C744F02E20BED000834F17 /* Pipify in Frameworks */,
68D569C02E1B415700A5BA36 /* LanguageSupport in Frameworks */,
68D569BE2E1B415700A5BA36 /* CodeEditorView in Frameworks */,
);
@ -262,6 +264,7 @@
packageProductDependencies = (
68D569BD2E1B415700A5BA36 /* CodeEditorView */,
68D569BF2E1B415700A5BA36 /* LanguageSupport */,
17C744EF2E20BED000834F17 /* Pipify */,
);
productName = StikJIT;
productReference = DC6F1D372D94EADD0071B2B6 /* StikDebug.app */;
@ -377,6 +380,7 @@
minimizedProjectReferenceProxies = 1;
packageReferences = (
68D569BC2E1B415700A5BA36 /* XCRemoteSwiftPackageReference "CodeEditorView" */,
17C744EE2E20BED000834F17 /* XCRemoteSwiftPackageReference "swiftui-pipify" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = DC6F1D382D94EADD0071B2B6 /* Products */;
@ -971,6 +975,14 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
17C744EE2E20BED000834F17 /* XCRemoteSwiftPackageReference "swiftui-pipify" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/hugeBlack/swiftui-pipify";
requirement = {
branch = main;
kind = branch;
};
};
68D569BC2E1B415700A5BA36 /* XCRemoteSwiftPackageReference "CodeEditorView" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mchakravarty/CodeEditorView";
@ -982,6 +994,11 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
17C744EF2E20BED000834F17 /* Pipify */ = {
isa = XCSwiftPackageProductDependency;
package = 17C744EE2E20BED000834F17 /* XCRemoteSwiftPackageReference "swiftui-pipify" */;
productName = Pipify;
};
68D569BD2E1B415700A5BA36 /* CodeEditorView */ = {
isa = XCSwiftPackageProductDependency;
package = 68D569BC2E1B415700A5BA36 /* XCRemoteSwiftPackageReference "CodeEditorView" */;

View file

@ -18,8 +18,6 @@
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>processing</string>
<string>fetch</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>

View file

@ -1,55 +0,0 @@
//
// 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,14 +69,14 @@ 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, \nYou are safe to close the PIP Window.")
self.logs.append("Script Execution Completed")
self.logs.append("You are safe to close the PIP Window.")
}
}
}
@ -99,6 +99,7 @@ struct RunJSViewPiP: View {
.onReceive(timer) { _ in
self.logs = model?.logs ?? []
}
.frame(width: 300, height: 150)
}
}

View file

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

View file

@ -1,29 +0,0 @@
//
// 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

@ -1,138 +0,0 @@
//
// 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

@ -7,6 +7,7 @@
import SwiftUI
import UniformTypeIdentifiers
import Pipify
extension UIDocumentPickerViewController {
@objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
@ -59,19 +60,6 @@ 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) {
@ -237,12 +225,7 @@ 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 {
@ -320,6 +303,9 @@ struct HomeView: View {
startJITInBackground(with: selectedBundle)
}
}
.pipify(isPresented: $isProcessing) {
RunJSViewPiP(model: $jsModel)
}
.sheet(isPresented: $scriptViewShow) {
NavigationView {
if let jsModel {
@ -328,6 +314,7 @@ struct HomeView: View {
ToolbarItem(placement: .topBarTrailing) {
Button("Done") {
scriptViewShow = false
isProcessing = false
}
}
}
@ -433,6 +420,7 @@ struct HomeView: View {
DispatchQueue.global(qos: .background).async {
do {
try jsModel?.runScript(path: selectedScriptURL)
isProcessing = false
} catch {
showAlert(title: "Error Occurred While Executing the Default Script.".localized, message: error.localizedDescription, showOk: true)
}

Binary file not shown.