├── porting ├── .gitignore ├── server │ ├── .gitignore │ ├── scrcpy-server │ └── src │ │ └── main │ │ └── java │ │ └── com │ │ └── genymobile │ │ └── scrcpy │ │ ├── Device.java │ │ └── Controller.java ├── src │ ├── main-porting.c │ ├── process-porting.c │ ├── audio_player-porting.c │ ├── decoder-porting.c │ ├── process-porting.hpp │ ├── demuxer-porting.c │ ├── controller-porting.c │ ├── display-porting.c │ ├── scrcpy-porting.c │ ├── screen-porting.c │ └── process-porting.cpp ├── scripts │ ├── make-ffmpeg.sh │ ├── make-scrcpy.sh │ ├── defines.sh │ └── make-libsdl.sh ├── include │ ├── scrcpy-porting.h │ └── porting.h ├── Makefile └── cmake │ └── CMakeLists.txt ├── scrcpy-ios ├── scrcpy-ios │ ├── en.lproj │ │ └── Localizable.strings │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── 20.png │ │ │ ├── 29.png │ │ │ ├── 40.png │ │ │ ├── 50.png │ │ │ ├── 57.png │ │ │ ├── 58.png │ │ │ ├── 60.png │ │ │ ├── 72.png │ │ │ ├── 76.png │ │ │ ├── 80.png │ │ │ ├── 87.png │ │ │ ├── 100.png │ │ │ ├── 1024.png │ │ │ ├── 114.png │ │ │ ├── 120.png │ │ │ ├── 144.png │ │ │ ├── 152.png │ │ │ ├── 167.png │ │ │ ├── 180.png │ │ │ └── Contents.json │ │ ├── More.imageset │ │ │ ├── More@3x.png │ │ │ └── Contents.json │ │ ├── Share.imageset │ │ │ ├── Share@3x.png │ │ │ └── Contents.json │ │ ├── BackIcon.imageset │ │ │ ├── BackIcon@3x.png │ │ │ └── Contents.json │ │ ├── HomeIcon.imageset │ │ │ ├── HomeIcon@3x.png │ │ │ └── Contents.json │ │ ├── Refresh.imageset │ │ │ ├── Refresh@3x.png │ │ │ └── Contents.json │ │ ├── TouchIcon.imageset │ │ │ ├── TouchIcon@3x.png │ │ │ └── Contents.json │ │ ├── disconnect.imageset │ │ │ ├── disconnect@3x.png │ │ │ └── Contents.json │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── Contents.json │ │ ├── KeyboardIcon.imageset │ │ │ ├── KeyboardIcon@3x.png │ │ │ └── Contents.json │ │ ├── SwitchAppIcon.imageset │ │ │ ├── SwitchAppIcon@3x.png │ │ │ └── Contents.json │ │ ├── DisconnectIcon.imageset │ │ │ ├── DisconnectIcon@3x.png │ │ │ └── Contents.json │ │ └── LaunchAppIcon.imageset │ │ │ ├── AppLaunchImage@2x.png │ │ │ ├── AppLaunchImage@3x.png │ │ │ └── Contents.json │ ├── ViewController.h │ ├── KeysViewController.h │ ├── LogsViewController.h │ ├── PairViewController.h │ ├── MenubarViewController.h │ ├── ScrcpySwitch.h │ ├── ScrcpyTextField.h │ ├── LogManager.h │ ├── SDL_uikitviewcontroller+Extend.h │ ├── ScrcpySwitch.m │ ├── SDLUIKitDelegate+Extend.h │ ├── Info.plist │ ├── ScrcpyTextField.m │ ├── main.m │ ├── LogManager.m │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ ├── ScrcpyClient.h │ ├── SDLUIKitDelegate+Extend.m │ ├── LogsViewController.m │ ├── zh-Hans.lproj │ │ └── Localizable.strings │ ├── SDL_uikitviewcontroller+Extend.m │ ├── UICommonUtils.h │ ├── MenubarViewController.m │ ├── PairViewController.m │ ├── KeysViewController.m │ └── ScrcpyClient.m ├── scrcpy-ios.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcuserdata │ │ │ └── ethan.xcuserdatad │ │ │ │ ├── UserInterfaceState.xcuserstate │ │ │ │ └── xcdebugger │ │ │ │ └── Expressions.xcexplist │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── xcuserdata │ │ └── ethan.xcuserdatad │ │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── scrcpy-ios.xcscheme └── scrcpy-vnc │ ├── VNCHTTPConnection.m │ ├── VNCViewController.h │ ├── VNCHTTPConnection.h │ ├── HTTPFileResponse+Extend.h │ ├── VNCBrowserViewController.h │ ├── HTTPFileResponse+Extend.m │ ├── VNCBrowserViewController.m │ └── VNCViewController.m ├── screenshots ├── telegram.png ├── screenshots.png └── Download_on_the_App_Store_Badge.png ├── Makefile ├── .gitignore ├── .gitmodules ├── LICENSE └── README.md /porting/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | -------------------------------------------------------------------------------- /porting/server/.gitignore: -------------------------------------------------------------------------------- 1 | scrcpy.zip 2 | scrcpy-*.* 3 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /screenshots/telegram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/screenshots/telegram.png -------------------------------------------------------------------------------- /porting/server/scrcpy-server: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/porting/server/scrcpy-server -------------------------------------------------------------------------------- /screenshots/screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/screenshots/screenshots.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /screenshots/Download_on_the_App_Store_Badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/screenshots/Download_on_the_App_Store_Badge.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/More.imageset/More@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/More.imageset/More@3x.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | libs: update-all libscrcpy 2 | 3 | update-all: 4 | git submodule update --init --recursive 5 | 6 | libscrcpy: 7 | mkdir -pv output/{iphone,android} 8 | make -C porting 9 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/Share.imageset/Share@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/Share.imageset/Share@3x.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/BackIcon.imageset/BackIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/BackIcon.imageset/BackIcon@3x.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/HomeIcon.imageset/HomeIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/HomeIcon.imageset/HomeIcon@3x.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/Refresh.imageset/Refresh@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/Refresh.imageset/Refresh@3x.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/TouchIcon.imageset/TouchIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/TouchIcon.imageset/TouchIcon@3x.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/disconnect.imageset/disconnect@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/disconnect.imageset/disconnect@3x.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/KeyboardIcon.imageset/KeyboardIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/KeyboardIcon.imageset/KeyboardIcon@3x.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/SwitchAppIcon.imageset/SwitchAppIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/SwitchAppIcon.imageset/SwitchAppIcon@3x.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/DisconnectIcon.imageset/DisconnectIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/DisconnectIcon.imageset/DisconnectIcon@3x.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/LaunchAppIcon.imageset/AppLaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/LaunchAppIcon.imageset/AppLaunchImage@2x.png -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/LaunchAppIcon.imageset/AppLaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios/Assets.xcassets/LaunchAppIcon.imageset/AppLaunchImage@3x.png -------------------------------------------------------------------------------- /porting/src/main-porting.c: -------------------------------------------------------------------------------- 1 | // 2 | // main-porting.c 3 | // scrcpy-mobile 4 | // 5 | // Created by Ethan on 2022/6/2. 6 | // 7 | 8 | #define main(...) scrcpy_main(__VA_ARGS__) 9 | 10 | #include "main.c" 11 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/2. 6 | // 7 | 8 | #import 9 | 10 | @interface ViewController : UIViewController 11 | @end 12 | 13 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios.xcodeproj/project.xcworkspace/xcuserdata/ethan.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsvn53/scrcpy-mobile/HEAD/scrcpy-ios/scrcpy-ios.xcodeproj/project.xcworkspace/xcuserdata/ethan.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-vnc/VNCHTTPConnection.m: -------------------------------------------------------------------------------- 1 | // 2 | // VNCHTTPConnection.m 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/15. 6 | // 7 | 8 | #import "VNCHTTPConnection.h" 9 | #import "HTTPFileResponse.h" 10 | 11 | @implementation VNCHTTPConnection 12 | @end 13 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-vnc/VNCViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // VNCViewController.h 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/15. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface VNCViewController : UIViewController 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/KeysViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // KeysViewController.h 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 27/12/22. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface KeysViewController : UIViewController 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/LogsViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // LogsViewController.h 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/7/20. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface LogsViewController : UIViewController 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/PairViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // PairViewController.h 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/7/18. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface PairViewController : UIViewController 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-vnc/VNCHTTPConnection.h: -------------------------------------------------------------------------------- 1 | // 2 | // VNCHTTPConnection.h 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/15. 6 | // 7 | 8 | #import "HTTPConnection.h" 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface VNCHTTPConnection : HTTPConnection 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-vnc/HTTPFileResponse+Extend.h: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPFileResponse+Extend.h 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/16. 6 | // 7 | 8 | #import "HTTPFileResponse.h" 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface HTTPFileResponse (Extend) 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/MenubarViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MenubarViewController.h 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/28. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface MenubarViewController : UIViewController 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/ScrcpySwitch.h: -------------------------------------------------------------------------------- 1 | // 2 | // ScrcpySwitch.h 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/14. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface ScrcpySwitch : UISwitch 13 | 14 | @property (nonatomic, copy) NSString *optionKey; 15 | 16 | -(void)updateOptionValue; 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/ScrcpyTextField.h: -------------------------------------------------------------------------------- 1 | // 2 | // ScrcpyTextField.h 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/12. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface ScrcpyTextField : UITextField 13 | 14 | @property (nonatomic, copy) NSString *optionKey; 15 | 16 | -(void)updateOptionValue; 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/LogManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // LogManager.h 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/7/19. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface LogManager : NSObject 13 | 14 | +(instancetype)sharedManager; 15 | 16 | -(void)startHandleLog; 17 | 18 | -(NSString *)recentLogs; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/SDL_uikitviewcontroller+Extend.h: -------------------------------------------------------------------------------- 1 | // 2 | // SDL_uikitviewcontroller+Extend.h 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/11. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface SDL_uikitviewcontroller : UIViewController 13 | @end 14 | 15 | @interface SDL_uikitviewcontroller (Extend) 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /porting/scripts/make-ffmpeg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OUTPUT=$(cd $OUTPUT && pwd); 4 | BUILD_DIR=$(mktemp -d -t ffmpeg); 5 | cd $BUILD_DIR; 6 | 7 | curl -O -L https://downloads.sourceforge.net/project/ffmpeg-ios/ffmpeg-ios-master.tar.bz2; 8 | bunzip2 ffmpeg-ios*.bz2; 9 | tar xvf ffmpeg-ios*.tar; 10 | cp -av FFmpeg-iOS/include/* $OUTPUT/include; 11 | cp -av FFmpeg-iOS/lib/* $OUTPUT/iphone; 12 | 13 | [[ -d "$BUILD_DIR" ]] && rm -rf $BUILD_DIR; 14 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/More.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "More@3x.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/Share.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "Share@3x.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-vnc/VNCBrowserViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // VNCBrowserViewController.h 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/15. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface VNCBrowserViewController : UIViewController 13 | 14 | @property (nonatomic, copy) NSString *vncURL; 15 | @property (nonatomic, assign) BOOL showsFullscreen; 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/Refresh.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "Refresh@3x.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/BackIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "BackIcon@3x.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/HomeIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "HomeIcon@3x.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/TouchIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "TouchIcon@3x.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/disconnect.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "disconnect@3x.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/KeyboardIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "KeyboardIcon@3x.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/DisconnectIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "DisconnectIcon@3x.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/SwitchAppIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "SwitchAppIcon@3x.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios.xcodeproj/xcuserdata/ethan.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | scrcpy-ios.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /porting/include/scrcpy-porting.h: -------------------------------------------------------------------------------- 1 | // 2 | // scrcpy.h 3 | // scrcpy-mobile 4 | // 5 | // Created by Ethan on 2022/6/2. 6 | // 7 | 8 | #ifndef scrcpy_h 9 | #define scrcpy_h 10 | 11 | #include 12 | int scrcpy_main(int argc, char *argv[]); 13 | 14 | enum ScrcpyStatus { 15 | ScrcpyStatusDisconnected = 0, 16 | ScrcpyStatusConnecting, 17 | ScrcpyStatusConnectingFailed, 18 | ScrcpyStatusConnected, 19 | }; 20 | void ScrcpyUpdateStatus(enum ScrcpyStatus status); 21 | 22 | #endif /* scrcpy_h */ 23 | -------------------------------------------------------------------------------- /porting/scripts/make-scrcpy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . $(dirname $0)/defines.sh; 4 | 5 | cmake_root=./cmake/out; 6 | 7 | # Clean built products 8 | [[ -d "$cmake_root" ]] && rm -rfv "$cmake_root"; 9 | mkdir -pv "$cmake_root"; 10 | 11 | cd "$cmake_root"; 12 | 13 | cmake .. -G Xcode -DCMAKE_TOOLCHAIN_FILE=$CMAKE_TOOLCHAIN_FILE -DPLATFORM=$PLATFORM \ 14 | -DDEPLOYMENT_TARGET=$DEPLOYMENT_TARGET; 15 | cmake --build . --config Debug --target scrcpy --parallel 8; 16 | find . -name "*.a" -exec cp -av {} $FULL_OUTPUT \; 17 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "LaunchImage@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "LaunchImage@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/LaunchAppIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "AppLaunchImage@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "AppLaunchImage@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/ScrcpySwitch.m: -------------------------------------------------------------------------------- 1 | // 2 | // ScrcpySwitch.m 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/14. 6 | // 7 | 8 | #import "ScrcpySwitch.h" 9 | #import "KFKeychain.h" 10 | 11 | @implementation ScrcpySwitch 12 | 13 | -(void)setOptionKey:(NSString *)optionKey { 14 | _optionKey = optionKey; 15 | NSNumber *optionValue = [KFKeychain loadObjectForKey:optionKey]; 16 | if (optionValue) { 17 | self.on = [optionValue boolValue]; 18 | } 19 | } 20 | 21 | -(void)updateOptionValue { 22 | [KFKeychain saveObject:@(self.on) forKey:self.optionKey]; 23 | } 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /porting/include/porting.h: -------------------------------------------------------------------------------- 1 | // 2 | // porting.h 3 | // scrcpy 4 | // 5 | // Created by Ethan on 2022/6/2. 6 | // 7 | 8 | #ifndef porting_h 9 | #define porting_h 10 | 11 | #include 12 | #include 13 | 14 | typedef GLfloat GLdouble; 15 | typedef double GLclampd; 16 | 17 | #include 18 | 19 | // Define NDEBUG will define assert -> (void)0, see assert.h 20 | // This will prevent to_fixed_point_16 crashed 21 | #define NDEBUG 1 22 | 23 | // Because conflicted function name with adb, rename it 24 | #define adb_connect(...) adb_connect__(__VA_ARGS__) 25 | 26 | #endif /* porting_h */ 27 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-vnc/HTTPFileResponse+Extend.m: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPFileResponse+Extend.m 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/16. 6 | // 7 | 8 | #import "HTTPFileResponse+Extend.h" 9 | 10 | @implementation HTTPFileResponse (Extend) 11 | 12 | -(NSDictionary *)httpHeaders { 13 | if ([self.filePath hasSuffix:@".svg"]) { 14 | return @{ 15 | @"Content-Type": @"image/svg+xml" 16 | }; 17 | } 18 | 19 | if ([self.filePath hasSuffix:@".js"]) { 20 | return @{ 21 | @"Content-Type": @"application/javascript" 22 | }; 23 | } 24 | 25 | return @{}; 26 | } 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios.xcodeproj/project.xcworkspace/xcuserdata/ethan.xcuserdatad/xcdebugger/Expressions.xcexplist: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | output 54 | -------------------------------------------------------------------------------- /porting/src/process-porting.c: -------------------------------------------------------------------------------- 1 | // 2 | // process-fix.c 3 | // scrcpy-module 4 | // 5 | // Created by Ethan on 2022/2/10. 6 | // 7 | 8 | #include "util/process.c" 9 | 10 | // Handle sc_process_execute_p to execute adb commands via libadb 11 | #define sc_process_execute_p(...) sc_process_execute_p__hijack(__VA_ARGS__) 12 | #define sc_pipe_read_all_intr(...) sc_pipe_read_all_intr__hijack(__VA_ARGS__) 13 | #define sc_process_wait(...) sc_process_wait__hijack(__VA_ARGS__) 14 | #define sc_process_terminate(...) sc_process_terminate__hijack(__VA_ARGS__) 15 | 16 | #include "sys/unix/process.c" 17 | #include "util/process_intr.c" 18 | 19 | #undef sc_process_execute_p 20 | #undef sc_pipe_read_all_intr 21 | #undef sc_process_wait 22 | #undef sc_process_terminate 23 | -------------------------------------------------------------------------------- /porting/src/audio_player-porting.c: -------------------------------------------------------------------------------- 1 | // 2 | // audio_player-porting.c 3 | // scrcpy-module 4 | // 5 | // Created by Ethan on 2023/5/21. 6 | // 7 | #include 8 | #include 9 | 10 | #define swr_convert(...) swr_convert_hijack(__VA_ARGS__) 11 | 12 | #include "audio_player.c" 13 | 14 | #undef swr_convert 15 | 16 | int swr_convert(struct SwrContext *s, uint8_t **out, int out_count, 17 | const uint8_t **in , int in_count); 18 | int swr_convert_hijack(struct SwrContext *s, uint8_t **out, int out_count, 19 | const uint8_t **in , int in_count) { 20 | // TODO: resample audio with accelerated API 21 | return swr_convert(s, out, out_count, in, in_count); 22 | } 23 | 24 | -------------------------------------------------------------------------------- /porting/src/decoder-porting.c: -------------------------------------------------------------------------------- 1 | // 2 | // decoder-porting.c 3 | // scrcpy-module 4 | // 5 | // Created by Ethan on 2022/6/8. 6 | // 7 | 8 | #define avcodec_send_packet(...) avcodec_send_packet_hijack(__VA_ARGS__) 9 | 10 | #include "decoder.c" 11 | 12 | #undef avcodec_send_packet 13 | 14 | bool ScrcpyEnableHardwareDecoding(void); 15 | int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt); 16 | int avcodec_send_packet_hijack(AVCodecContext *avctx, const AVPacket *avpkt) { 17 | int ret = avcodec_send_packet(avctx, avpkt); 18 | 19 | if (ret == AVERROR_UNKNOWN && ScrcpyEnableHardwareDecoding()) { 20 | // Fix Hardware Decoding Error After Return From Background 21 | return 0; 22 | } 23 | 24 | return ret; 25 | } 26 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/SDLUIKitDelegate+Extend.h: -------------------------------------------------------------------------------- 1 | // 2 | // SDLUIKitDelegate+Extend.h 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/11. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | #define ScrcpyConnectWithSchemeNotification @"ScrcpyConnectWithSchemeNotification" 13 | #define ScrcpyConnectWithSchemeURLKey @"URL" 14 | 15 | #define ScrcpySwitchModeCommand @"switch" 16 | #define ScrcpySwitchModeKey @"mode" 17 | #define ScrcpyModeVNC @"vnc" 18 | #define ScrcpyModeADB @"adb" 19 | 20 | void ScrcpyReloadViewController(UIWindow *window); 21 | 22 | @interface SDLUIKitDelegate : NSObject 23 | @end 24 | 25 | @interface SDLUIKitDelegate (Extend) 26 | @end 27 | 28 | NS_ASSUME_NONNULL_END 29 | -------------------------------------------------------------------------------- /porting/src/process-porting.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // process-porting.hpp 3 | // scrcpy 4 | // 5 | // Created by Ethan on 2022/3/19. 6 | // 7 | 8 | #ifndef process_porting_hpp 9 | #define process_porting_hpp 10 | 11 | #include 12 | #include 13 | 14 | #ifdef __cplusplus 15 | extern "C" { 16 | #endif 17 | 18 | typedef pid_t sc_pid; 19 | typedef int sc_exit_code; 20 | typedef int sc_pipe; 21 | 22 | int 23 | sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags, 24 | int *pin, int *pout, int *perr); 25 | ssize_t 26 | sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, 27 | char *data, size_t len); 28 | sc_exit_code 29 | sc_process_wait(pid_t pid, bool close); 30 | bool 31 | sc_process_terminate(pid_t pid); 32 | 33 | #ifdef __cplusplus 34 | } 35 | #endif 36 | 37 | #endif /* process_porting_hpp */ 38 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BGTaskSchedulerPermittedIdentifiers 6 | 7 | com.mobile.scrcpy-ios.task 8 | 9 | CFBundleURLTypes 10 | 11 | 12 | CFBundleURLName 13 | com.mobile.scrcpy-ios 14 | CFBundleURLSchemes 15 | 16 | scrcpy2 17 | 18 | 19 | 20 | ITSAppUsesNonExemptEncryption 21 | 22 | UIBackgroundModes 23 | 24 | fetch 25 | processing 26 | 27 | UIViewControllerBasedStatusBarAppearance 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/ScrcpyTextField.m: -------------------------------------------------------------------------------- 1 | // 2 | // ScrcpyTextField.m 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/12. 6 | // 7 | 8 | #import "ScrcpyTextField.h" 9 | #import "KFKeychain.h" 10 | 11 | @implementation ScrcpyTextField 12 | 13 | -(void)setOptionKey:(NSString *)optionKey { 14 | _optionKey = optionKey; 15 | self.text = [KFKeychain loadObjectForKey:optionKey]; 16 | } 17 | 18 | -(void)updateOptionValue { 19 | [KFKeychain saveObject:self.text forKey:self.optionKey]; 20 | } 21 | 22 | -(CGRect)textRectForBounds:(CGRect)bounds { 23 | return UIEdgeInsetsInsetRect(bounds, UIEdgeInsetsMake(0, 6, 0, 6)); 24 | } 25 | 26 | -(CGRect)placeholderRectForBounds:(CGRect)bounds { 27 | return UIEdgeInsetsInsetRect(bounds, UIEdgeInsetsMake(0, 6, 0, 6)); 28 | } 29 | 30 | -(CGRect)editingRectForBounds:(CGRect)bounds { 31 | return UIEdgeInsetsInsetRect(bounds, UIEdgeInsetsMake(0, 6, 0, 6)); 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "scrcpy"] 2 | path = scrcpy 3 | url = https://github.com/Genymobile/scrcpy.git 4 | [submodule "external/adb-mobile"] 5 | path = external/adb-mobile 6 | url = https://github.com/wsvn53/adb-mobile.git 7 | [submodule "ios-cmake"] 8 | path = ios-cmake 9 | url = https://github.com/leetal/ios-cmake.git 10 | [submodule "scrcpy-ios/chainable-view"] 11 | path = scrcpy-ios/chainable-view 12 | url = https://github.com/wsvn53/chainable-view.git 13 | [submodule "scrcpy-ios/Keychain-iOS-ObjC"] 14 | path = scrcpy-ios/Keychain-iOS-ObjC 15 | url = https://github.com/Keyflow/Keychain-iOS-ObjC.git 16 | [submodule "scrcpy-ios/MBProgressHUD"] 17 | path = scrcpy-ios/MBProgressHUD 18 | url = https://github.com/jdg/MBProgressHUD.git 19 | [submodule "scrcpy-ios/CocoaHTTPServer"] 20 | path = scrcpy-ios/CocoaHTTPServer 21 | url = https://github.com/robbiehanson/CocoaHTTPServer.git 22 | [submodule "scrcpy-ios/noVNC"] 23 | path = scrcpy-ios/noVNC 24 | url = https://github.com/novnc/noVNC.git 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ethan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /porting/scripts/defines.sh: -------------------------------------------------------------------------------- 1 | # Common Supports: 2 | # - TARGET=lz4/iphoneos/arm64 3 | # - OUTPUT=... 4 | 5 | [[ -z "$TARGET" ]] && echo "** ERROR: TARGET is REQUIRED." && exit 1; 6 | [[ -z "$OUTPUT" ]] && echo "** ERROR: OUTPUT is REQUIRED." && exit 1; 7 | 8 | LIB_NAME=$(echo "$TARGET" | cut -d/ -f1); 9 | SDK_NAME=$(echo "$TARGET" | cut -d/ -f2); 10 | ARCH_NAME=$(echo "$TARGET" | cut -d/ -f3); 11 | 12 | # Src root 13 | SOURCE_ROOT=$(cd $(dirname $0)/../.. && pwd); 14 | 15 | # Porting root 16 | PORTING_ROOT=$SOURCE_ROOT/porting; 17 | 18 | # Prepare output path 19 | FULL_OUTPUT=$(cd $OUTPUT && pwd)/$SDK_NAME/$ARCH_NAME; 20 | [[ ! -d $FULL_OUTPUT ]] && mkdir -p $FULL_OUTPUT; 21 | 22 | # For iphone, change to platform 23 | PLATFORM=$([[ $SDK_NAME == iphoneos ]] && echo "OS64" || echo "SIMULATOR64"); 24 | 25 | # Setup iphone deploy target 26 | DEPLOYMENT_TARGET=11.0; 27 | 28 | # Setup toolchains 29 | if [[ $SDK_NAME == *iphone* ]]; then 30 | CMAKE_TOOLCHAIN_FILE=$SOURCE_ROOT/ios-cmake/ios.toolchain.cmake; 31 | fi 32 | 33 | # Print summary 34 | echo " - Lib Name: $LIB_NAME"; 35 | echo " - SDK Name: $SDK_NAME"; 36 | echo " - Arch Name: $ARCH_NAME"; 37 | echo " - CMake Toolchain: $CMAKE_TOOLCHAIN_FILE"; 38 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/2. 6 | // 7 | 8 | #import 9 | #import 10 | #import "KFKeychain.h" 11 | #import "config.h" 12 | 13 | #import "SDLUIKitDelegate+Extend.h" 14 | #import "ViewController.h" 15 | #import "VNCViewController.h" 16 | 17 | int main(int argc, char * argv[]) { 18 | NSLog(@"Hello scrcpy v%s", SCRCPY_VERSION); 19 | 20 | static UIWindow *window = nil; 21 | window = window ?: [[UIWindow alloc] init]; 22 | ScrcpyReloadViewController(window); 23 | [window makeKeyAndVisible]; 24 | 25 | return 0; 26 | } 27 | 28 | void ScrcpyReloadViewController(UIWindow *window) { 29 | UIViewController *mainController = nil; 30 | NSString *mode = [KFKeychain loadObjectForKey:ScrcpySwitchModeKey]; 31 | if ([mode isEqualToString:@"adb"]) { 32 | // mainController = [[ScrcpyViewController alloc] initWithNibName:nil bundle:nil]; 33 | mainController = [[ViewController alloc] initWithNibName:nil bundle:nil]; 34 | } else if (mode.length == 0 || [mode isEqualToString:@"vnc"]) { 35 | mainController = [[VNCViewController alloc] initWithNibName:nil bundle:nil]; 36 | } 37 | window.rootViewController = [[UINavigationController alloc] initWithRootViewController:mainController]; 38 | } 39 | -------------------------------------------------------------------------------- /porting/src/demuxer-porting.c: -------------------------------------------------------------------------------- 1 | // 2 | // demuxer-porting.c 3 | // scrcpy-module 4 | // 5 | // Created by Ethan on 2023/5/20. 6 | // 7 | 8 | #define avcodec_alloc_context3(...) avcodec_alloc_context3_hijack(__VA_ARGS__) 9 | 10 | #include "demuxer.c" 11 | 12 | #undef avcodec_alloc_context3 13 | 14 | bool ScrcpyEnableHardwareDecoding(void); 15 | 16 | AVCodecContext *avcodec_alloc_context3(const AVCodec *codec); 17 | AVCodecContext *avcodec_alloc_context3_hijack(const AVCodec *codec) { 18 | AVCodecContext *context = avcodec_alloc_context3(codec); 19 | 20 | if (context->codec_type != AVMEDIA_TYPE_VIDEO || 21 | ScrcpyEnableHardwareDecoding() == false) { 22 | printf("hardware decoding is disabled\n"); 23 | return context; 24 | } 25 | 26 | // Create context with hardware decoder 27 | AVBufferRef *codec_buf; 28 | const char *codecName = av_hwdevice_get_type_name(AV_HWDEVICE_TYPE_VIDEOTOOLBOX); 29 | enum AVHWDeviceType type = av_hwdevice_find_type_by_name(codecName); 30 | int ret = av_hwdevice_ctx_create(&codec_buf, type, NULL, NULL, 0); 31 | if (ret == 0) { 32 | context->hw_device_ctx = av_buffer_ref(codec_buf); 33 | return context; 34 | } 35 | 36 | printf("[WARN] Init hardware decoder FAILED, fallback to foftware decoder."); 37 | return context; 38 | } 39 | 40 | -------------------------------------------------------------------------------- /porting/Makefile: -------------------------------------------------------------------------------- 1 | all: scrcpy-init adb-mobile libsdl ffmpeg scrcpy scrcpy-server 2 | 3 | scrcpy-init: 4 | cd ../scrcpy && rm -rf x && meson x --buildtype release --strip -Db_lto=true -Dportable=true -Dusb=false 5 | 6 | adb-mobile: 7 | make -C ../external/adb-mobile 8 | 9 | libsdl: 10 | OUTPUT=../output bash ./scripts/make-libsdl.sh 11 | 12 | ffmpeg: 13 | OUTPUT=../output bash ./scripts/make-ffmpeg.sh 14 | 15 | scrcpy: 16 | OUTPUT=../output TARGET=scrcpy/iphoneos/arm64 bash ./scripts/make-scrcpy.sh 17 | OUTPUT=../output TARGET=scrcpy/iphonesimulator/x86_64 bash ./scripts/make-scrcpy.sh 18 | lipo -create ../output/*/*/libscrcpy.a -output ../output/iphone/libscrcpy.a 19 | 20 | scrcpy-server: 21 | set -x && \ 22 | export SCRCPY_VERSION=2.3 && \ 23 | export ANDROID_SDK_ROOT=~/Library/Android/sdk && \ 24 | export JAVA_HOME="$$(/usr/libexec/java_home --version 1.11)" && \ 25 | export PATH="$$JAVA_HOME/bin:$$PATH" && \ 26 | cd server && rm -rf scrcpy-* && \ 27 | curl -o scrcpy.zip -L https://github.com/Genymobile/scrcpy/archive/refs/tags/v$$SCRCPY_VERSION.zip && \ 28 | unzip scrcpy.zip && find src -name "*.java" -exec cp -v {} scrcpy-$$SCRCPY_VERSION/server/{} \; && \ 29 | cd scrcpy-$$SCRCPY_VERSION && \ 30 | meson setup x --buildtype=release --strip -Db_lto=true -Dcompile_app=false -Dcompile_server=true && \ 31 | sed -ibak 's/exit/echo/g' server/scripts/build-wrapper.sh && \ 32 | ninja -Cx -v && \ 33 | find . -type f -name "scrcpy-server" -exec cp -v {} .. \; 34 | -------------------------------------------------------------------------------- /porting/src/controller-porting.c: -------------------------------------------------------------------------------- 1 | // 2 | // controller-porting.c 3 | // scrcpy-mobile 4 | // 5 | // Created by Ethan on 2022/6/2. 6 | // 7 | 8 | #define sc_controller_push_msg(...) sc_controller_push_msg_hijack(__VA_ARGS__) 9 | 10 | #include "controller.c" 11 | 12 | #undef sc_controller_push_msg 13 | 14 | // Defined in screen-porting.m 15 | #import "screen.h" 16 | struct sc_screen * 17 | sc_screen_current_screen(struct sc_screen *screen); 18 | 19 | // Fix negative point values and larger than screen size 20 | bool sc_controller_push_msg(struct sc_controller *controller, 21 | struct sc_control_msg *msg) { 22 | if (msg->type == SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT) { 23 | // x/y is negative 24 | msg->inject_touch_event.position.point.x = msg->inject_touch_event.position.point.x < 0 ? 0 : msg->inject_touch_event.position.point.x; 25 | msg->inject_touch_event.position.point.y = msg->inject_touch_event.position.point.y < 0 ? 0 : msg->inject_touch_event.position.point.y; 26 | 27 | // x/y exceed max frame size 28 | struct sc_screen *screen = sc_screen_current_screen(NULL); 29 | if (screen != NULL) { 30 | struct sc_size screen_size; 31 | screen_size.width = screen->frame->width; 32 | screen_size.height = screen->frame->height; 33 | 34 | msg->inject_touch_event.position.point.x = msg->inject_touch_event.position.point.x > screen_size.width ? screen_size.width : msg->inject_touch_event.position.point.x; 35 | msg->inject_touch_event.position.point.y = msg->inject_touch_event.position.point.y > screen_size.height ? screen_size.height : msg->inject_touch_event.position.point.y; 36 | } 37 | } 38 | 39 | return sc_controller_push_msg_hijack(controller, msg); 40 | } 41 | -------------------------------------------------------------------------------- /porting/src/display-porting.c: -------------------------------------------------------------------------------- 1 | // 2 | // screen-porting.c 3 | // scrcpy-module 4 | // 5 | // Created by Ethan on 2022/6/3. 6 | // 7 | 8 | #include "stdbool.h" 9 | #include 10 | #include 11 | 12 | bool ScrcpyEnableHardwareDecoding(void); 13 | int SDL_UpdateYUVTexture_hijack(SDL_Texture * texture, 14 | const SDL_Rect * rect, 15 | const Uint8 *Yplane, int Ypitch, 16 | const Uint8 *Uplane, int Upitch, 17 | const Uint8 *Vplane, int Vpitch); 18 | void SDL_RenderPresent_hijack(SDL_Renderer * renderer); 19 | 20 | #define SDL_UpdateYUVTexture(...) SDL_UpdateYUVTexture_hijack(__VA_ARGS__) 21 | #define SDL_RenderPresent(...) SDL_RenderPresent_hijack(__VA_ARGS__) 22 | 23 | #include "display.c" 24 | 25 | #undef SDL_UpdateYUVTexture 26 | #undef SDL_RenderPresent 27 | 28 | // Hijack SDL_UpdateYUVTexture 29 | int SDL_UpdateYUVTexture_hijack(SDL_Texture * texture, 30 | const SDL_Rect * rect, 31 | const Uint8 *Yplane, int Ypitch, 32 | const Uint8 *Uplane, int Upitch, 33 | const Uint8 *Vplane, int Vpitch) 34 | { 35 | if (ScrcpyEnableHardwareDecoding()) { return 0; } 36 | return SDL_UpdateYUVTexture(texture, rect, Yplane, Ypitch, Uplane, Upitch, Vplane, Vpitch); 37 | } 38 | 39 | void SDL_UpdateCommandGeneration(SDL_Renderer * renderer); 40 | void SDL_RenderPresent_hijack(SDL_Renderer * renderer) { 41 | if (ScrcpyEnableHardwareDecoding()) { 42 | // Update renderer_command_generation to fix memory leak when Destory_Texture 43 | SDL_UpdateCommandGeneration(renderer); 44 | return; 45 | } 46 | SDL_RenderPresent(renderer); 47 | } 48 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-vnc/VNCBrowserViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // VNCBrowserViewController.m 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/15. 6 | // 7 | 8 | #import "VNCBrowserViewController.h" 9 | #import "CVCreate.h" 10 | #import 11 | 12 | @interface VNCBrowserViewController () 13 | 14 | @property (nonatomic, strong) WKWebView *webView; 15 | 16 | @end 17 | 18 | @implementation VNCBrowserViewController 19 | 20 | - (void)viewDidLoad { 21 | [super viewDidLoad]; 22 | [self setupViews]; 23 | [self loadVNCWithURL:[NSURL URLWithString:self.vncURL]]; 24 | } 25 | 26 | -(void)setupViews { 27 | self.title = self.title.length > 0 ? self.title : @"Remote VNC"; 28 | self.view.backgroundColor = UIColor.whiteColor; 29 | 30 | self.webView = [[WKWebView alloc] initWithFrame:(CGRectZero)]; 31 | self.webView.navigationDelegate = (id)self; 32 | CVCreate.withView(self.webView).addToView(self.view) 33 | .centerXAnchor(self.view.centerXAnchor, 0) 34 | .centerYAnchor(self.view.centerYAnchor, 0) 35 | .widthAnchor(self.view.widthAnchor, 0) 36 | .heightAnchor(self.view.heightAnchor, 0); 37 | 38 | if (self.showsFullscreen) { 39 | [self.navigationController setNavigationBarHidden:YES animated:YES]; 40 | CVCreate.UIImageView([UIImage imageNamed:@"disconnect"]) 41 | .addToView(self.view) 42 | .backgroundColor(UIColor.grayColor) 43 | .cornerRadius(10) 44 | .rightAnchor(self.view.rightAnchor, -20) 45 | .topAnchor(self.view.topAnchor, 60) 46 | .click(self, @selector(disconnect)); 47 | } 48 | } 49 | 50 | -(void)loadVNCWithURL:(NSURL *)url { 51 | [self.webView loadRequest:[NSURLRequest requestWithURL:url]]; 52 | } 53 | 54 | -(void)disconnect { 55 | [self.navigationController popViewControllerAnimated:YES]; 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/LogManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // LogManager.m 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/7/19. 6 | // 7 | 8 | #import "LogManager.h" 9 | 10 | #define kRecentLogsLimit 512*1024 11 | 12 | @interface LogManager () 13 | 14 | @property (nonatomic, copy) NSString *logPath; 15 | 16 | @end 17 | 18 | @implementation LogManager 19 | 20 | +(instancetype)sharedManager { 21 | static LogManager *mananger = nil; 22 | static dispatch_once_t onceToken; 23 | dispatch_once(&onceToken, ^{ 24 | mananger = [[LogManager alloc] init]; 25 | }); 26 | return mananger; 27 | } 28 | 29 | -(NSString *)logPath { 30 | return _logPath ?: ({ 31 | NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; 32 | [formatter setDateFormat:@"yyyy-MM"]; 33 | NSString *dateString = [formatter stringFromDate:[NSDate date]]; 34 | NSArray *allPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 35 | NSString *documentsDirectory = [allPaths objectAtIndex:0]; 36 | NSString *logName = [NSString stringWithFormat:@"scrcpy@%@", dateString]; 37 | _logPath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.log", logName]]; 38 | }); 39 | } 40 | 41 | -(void)startHandleLog { 42 | freopen([self.logPath cStringUsingEncoding:NSASCIIStringEncoding],"a+",stderr); 43 | freopen([self.logPath cStringUsingEncoding:NSASCIIStringEncoding],"a+",stdout); 44 | } 45 | 46 | -(NSString *)recentLogs { 47 | NSFileHandle *logHandle = [NSFileHandle fileHandleForReadingAtPath:self.logPath]; 48 | NSDictionary *attrs = [NSFileManager.defaultManager attributesOfItemAtPath:self.logPath error:nil]; 49 | NSInteger fileSize = [attrs[NSFileSize] integerValue]; 50 | NSLog(@"LogPath: %@, Size: %@", self.logPath, @(fileSize)); 51 | if (fileSize > kRecentLogsLimit) { 52 | [logHandle seekToFileOffset:fileSize-kRecentLogsLimit]; 53 | } 54 | NSData *logData = [logHandle readDataOfLength:kRecentLogsLimit]; 55 | return [[NSString alloc] initWithData:logData encoding:NSUTF8StringEncoding]; 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /porting/scripts/make-libsdl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x; 4 | 5 | OUTPUT=$(cd $OUTPUT && pwd); 6 | BUILD_DIR=$(mktemp -d -t SDL); 7 | cd $BUILD_DIR; 8 | 9 | curl -O https://www.libsdl.org/release/SDL2-2.0.22.tar.gz; 10 | tar xzvf SDL*.tar.gz; 11 | 12 | # Add Function SDL_UpdateCommandGeneration 13 | echo "=> Add Function SDL_UpdateCommandGeneration" 14 | echo "$(cat << EOF 15 | void 16 | SDL_UpdateCommandGeneration(SDL_Renderer *renderer) { 17 | renderer->render_command_generation++; 18 | } 19 | EOF 20 | )" >> SDL2-*/src/render/SDL_render.c; 21 | 22 | # Build iOS Libraries 23 | echo "=> Building for iOS.."; 24 | 25 | xcodebuild clean build OTHER_CFLAGS="-fembed-bitcode" \ 26 | BUILD_DIR=$BUILD_DIR/build \ 27 | ARCHS="arm64" \ 28 | CONFIGURATION=Debug \ 29 | GCC_PREPROCESSOR_DEFINITIONS='CFRunLoopRunInMode=CFRunLoopRunInMode_fix' \ 30 | -project SDL2-*/Xcode/SDL/SDL.xcodeproj -scheme "Static Library-iOS" -sdk iphoneos; 31 | xcodebuild clean build OTHER_CFLAGS="-fembed-bitcode" \ 32 | BUILD_DIR=$BUILD_DIR/build \ 33 | ARCHS="x86_64" \ 34 | CONFIGURATION=Debug \ 35 | GCC_PREPROCESSOR_DEFINITIONS='CFRunLoopRunInMode=CFRunLoopRunInMode_fix' \ 36 | -project SDL2-*/Xcode/SDL/SDL.xcodeproj -scheme "Static Library-iOS" -sdk iphonesimulator; 37 | 38 | lipo -create build/*/libSDL2.a -output build/libSDL2.a; 39 | file build/libSDL2.a; 40 | 41 | [[ -d "$OUTPUT/include/SDL2" ]] || mkdir -pv $OUTPUT/include/SDL2; 42 | [[ -d "$OUTPUT/iphone" ]] || mkdir -pv $OUTPUT/iphone; 43 | [[ -d "$OUTPUT/iphone" ]] && { 44 | cp -v build/libSDL2.a $OUTPUT/iphone; 45 | cp -v SDL2-*/include/*.h $OUTPUT/include/SDL2; 46 | } 47 | 48 | # echo "=> Building for Android.."; 49 | 50 | # # if mac arm64 host, run with x86_64 by arch command 51 | # ARCH_RUN=""; 52 | # uname -m | grep arm64 && { 53 | # ARCH_RUN="arch -x86_64"; 54 | # } 55 | 56 | # # Check android sdk home 57 | # [[ -z "$ANDROID_HOME" ]] && echo "No ANDROID_HOME set" && exit 1; 58 | 59 | # PATH=$PATH:$(dirname "$(find $ANDROID_HOME -name "ndk-build" | tail -n1)") $ARCH_RUN sh ./SDL2-*/build-scripts/androidbuildlibs.sh 60 | # [[ -d $OUTPUT/lib/android ]] || mkdir -pv $OUTPUT/lib/android/{arm64-v8a,armeabi-v7a,x86,x86_64}; 61 | 62 | # cp -av SDL2-*/build/android/lib/arm64-v8a/* $OUTPUT/lib/android/arm64-v8a; 63 | # cp -av SDL2-*/build/android/lib/armeabi-v7a/* $OUTPUT/lib/android/armeabi-v7a; 64 | # cp -av SDL2-*/build/android/lib/x86/* $OUTPUT/lib/android/x86; 65 | # cp -av SDL2-*/build/android/lib/x86_64/* $OUTPUT/lib/android/x86_64; 66 | 67 | [[ -d "$BUILD_DIR" ]] && rm -rf $BUILD_DIR; 68 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/ScrcpyClient.h: -------------------------------------------------------------------------------- 1 | // 2 | // ScrcpyClient.h 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/2. 6 | // 7 | 8 | #import 9 | #import 10 | #import 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | #define ScrcpySharedClient [ScrcpyClient sharedClient] 15 | 16 | @interface ScrcpyClient : NSObject 17 | // ADB Setup 18 | @property (nonatomic, assign) NSInteger adbDaemonPort; 19 | @property (nonatomic, copy) NSString *adbHomePath; 20 | 21 | // ADB Callbacks 22 | @property (nonatomic, copy) void (^onADBConnecting)(NSString *serial); 23 | @property (nonatomic, copy) void (^onADBConnected)(NSString *serial); 24 | @property (nonatomic, copy) void (^onADBUnauthorized)(NSString *serial); 25 | @property (nonatomic, copy) void (^onADBDisconnected)(NSString *serial); 26 | @property (nonatomic, copy) void (^onADBConnectFailed)(NSString *serial, NSString *message); 27 | 28 | // Scrcpy Callbacks 29 | @property (nonatomic, copy) void (^onScrcpyConnected)(NSString *serial); 30 | @property (nonatomic, copy) void (^onScrcpyConnectFailed)(NSString *serial); 31 | @property (nonatomic, copy) void (^onScrcpyDisconnected)(NSString *serial); 32 | 33 | // Pending URL Scheme 34 | @property (nonatomic, strong, nullable) NSURL *pendingScheme; 35 | 36 | // Switch Always Show Navigation Buttons 37 | @property (nonatomic, assign) BOOL shouldAlwaysShowNavButtons; 38 | 39 | // Switch to enable power saving mode on iPhone, with lower refresh rate and lower interactive response 40 | @property (nonatomic, assign) BOOL enablePowerSavingMode; 41 | 42 | // Shared instance 43 | +(instancetype)sharedClient; 44 | 45 | // ADB trace log 46 | -(void)enableADBVerbose; 47 | 48 | // Start ADB Server 49 | -(void)startADBServer; 50 | 51 | // Start scrcpy 52 | -(void)startWith:(NSString *)adbHost adbPort:(NSString *)adbPort options:(NSArray *)scrcpyOptions; 53 | 54 | // Stop scrcpy 55 | -(void)stopScrcpy; 56 | 57 | // Execute ADB commands 58 | -(BOOL)adbExecute:(NSArray *)commands message:(NSString *_Nullable*_Nullable)message; 59 | 60 | // Check pending scheme and start scrcpy 61 | -(void)checkStartScheme; 62 | 63 | // Default scrcpy options 64 | -(NSArray *)defaultScrcpyOptions; 65 | 66 | // Set scrcpy options 67 | -(NSArray *)setScrcpyOption:(NSArray *)options name:(NSString *)name value:(NSString *)value; 68 | 69 | // Send Home Button 70 | -(void)sendHomeButton; 71 | 72 | // Send Back Button 73 | -(void)sendBackButton; 74 | 75 | // Send SwitchApp Button 76 | -(void)sendSwitchAppButton; 77 | 78 | // Send Key Events 79 | -(void)sendKeycodeEvent:(SDL_Scancode)scancode keycode:(SDL_Keycode)keycode keymod:(SDL_Keymod)keymod; 80 | 81 | @end 82 | 83 | NS_ASSUME_NONNULL_END 84 | -------------------------------------------------------------------------------- /porting/src/scrcpy-porting.c: -------------------------------------------------------------------------------- 1 | // 2 | // scrcpy-porting.c 3 | // scrcpy-mobile 4 | // 5 | // Created by Ethan on 2022/6/2. 6 | // 7 | 8 | #include "scrcpy-porting.h" 9 | 10 | #define sc_server_init(...) sc_server_init_hijack(__VA_ARGS__) 11 | #define sc_delay_buffer_init(...) sc_delay_buffer_init_hijack(__VA_ARGS__) 12 | 13 | #include "scrcpy.c" 14 | 15 | #undef sc_server_init 16 | #undef sc_delay_buffer_init 17 | 18 | __attribute__((weak)) 19 | void ScrcpyUpdateStatus(enum ScrcpyStatus status) { 20 | printf("ScrcpyUpdateStatus: %d\n", status); 21 | } 22 | 23 | static void 24 | sc_server_on_connection_failed_hijack(struct sc_server *server, void *userdata) { 25 | sc_server_on_connection_failed(server, userdata); 26 | 27 | // Notify update status 28 | ScrcpyUpdateStatus(ScrcpyStatusConnectingFailed); 29 | } 30 | 31 | static void 32 | sc_server_on_disconnected_hijack(struct sc_server *server, void *userdata) { 33 | sc_server_on_disconnected(server, userdata); 34 | 35 | // Fixed here, send quit event 36 | SDL_Event event; 37 | event.type = SDL_QUIT; 38 | SDL_PushEvent(&event); 39 | 40 | // Notify update status 41 | ScrcpyUpdateStatus(ScrcpyStatusDisconnected); 42 | } 43 | 44 | static void 45 | sc_server_on_connected_hijack(struct sc_server *server, void *userdata) { 46 | sc_server_on_connected(server, userdata); 47 | 48 | // Notify update status 49 | ScrcpyUpdateStatus(ScrcpyStatusConnected); 50 | } 51 | 52 | // Handle sc_server_init to change cbs->on_disconnected callback 53 | // in order to quit normally when occur some unexpect network close like in sleep mode 54 | bool 55 | sc_server_init(struct sc_server *server, const struct sc_server_params *params, 56 | const struct sc_server_callbacks *cbs, void *cbs_userdata); 57 | bool 58 | sc_server_init_hijack(struct sc_server *server, const struct sc_server_params *params, 59 | const struct sc_server_callbacks *cbs, void *cbs_userdata) { 60 | static const struct sc_server_callbacks cbs_fixed = { 61 | .on_connection_failed = sc_server_on_connection_failed_hijack, 62 | .on_connected = sc_server_on_connected_hijack, 63 | .on_disconnected = sc_server_on_disconnected_hijack, 64 | }; 65 | return sc_server_init(server, params, &cbs_fixed, cbs_userdata); 66 | } 67 | 68 | // Handle sc_delay_buffer_init to reset deley_buffer stopped status 69 | // this can fix the issue: cannot continue video and audio buffer after re-connect 70 | void 71 | sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay, 72 | bool first_frame_asap); 73 | void 74 | sc_delay_buffer_init_hijack(struct sc_delay_buffer *db, sc_tick delay, 75 | bool first_frame_asap) { 76 | sc_delay_buffer_init(db, delay, first_frame_asap); 77 | db->stopped = false; 78 | } 79 | -------------------------------------------------------------------------------- /porting/src/screen-porting.c: -------------------------------------------------------------------------------- 1 | // 2 | // screen-porting.c 3 | // scrcpy-module 4 | // 5 | // Created by Ethan on 2022/6/3. 6 | // 7 | 8 | #include "stdbool.h" 9 | 10 | bool ScrcpyEnableHardwareDecoding(void); 11 | 12 | #define sc_screen_init(...) sc_screen_init_orig(__VA_ARGS__) 13 | #define sc_frame_buffer_consume(...) sc_frame_buffer_consume_hijack(__VA_ARGS__) 14 | #define sc_screen_handle_event(...) sc_screen_handle_event_hijack(__VA_ARGS__) 15 | 16 | #include "screen.c" 17 | 18 | #undef sc_screen_init 19 | #undef sc_frame_buffer_consume 20 | #undef sc_screen_handle_event 21 | 22 | struct sc_screen * 23 | sc_screen_current_screen(struct sc_screen *screen) { 24 | static struct sc_screen *current_screen; 25 | if (screen != NULL) { 26 | current_screen = screen; 27 | } 28 | return current_screen; 29 | } 30 | 31 | __attribute__((weak)) 32 | float screen_scale(void) { 33 | return 2.f; 34 | } 35 | 36 | bool 37 | sc_screen_init(struct sc_screen *screen, 38 | const struct sc_screen_params *params) { 39 | bool ret = sc_screen_init_orig(screen, params); 40 | 41 | // Set renderer scale 42 | SDL_RenderSetScale(screen->display.renderer, screen_scale(), screen_scale()); 43 | 44 | // Save current screen pointer 45 | sc_screen_current_screen(screen); 46 | 47 | return ret; 48 | } 49 | 50 | __attribute__((weak)) 51 | void ScrcpyHandleFrame(AVFrame *frame) {} 52 | 53 | void 54 | sc_frame_buffer_consume(struct sc_frame_buffer *fb, AVFrame *dst); 55 | // Hijack sc_video_buffer_consume to convert NV12 pixels 56 | void 57 | sc_frame_buffer_consume_hijack(struct sc_frame_buffer *fb, AVFrame *dst) { 58 | sc_frame_buffer_consume(fb, dst); 59 | 60 | // Handle hardware frame render 61 | if (ScrcpyEnableHardwareDecoding()) ScrcpyHandleFrame(dst); 62 | } 63 | 64 | bool 65 | sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { 66 | // Handle Clipboard Event to Sync Clipboard to Remote 67 | if (event->type == SDL_CLIPBOARDUPDATE) { 68 | char *text = SDL_GetClipboardText(); 69 | if (!text) { 70 | LOGW("Could not get clipboard text: %s", SDL_GetError()); 71 | return false; 72 | } 73 | 74 | char *text_dup = strdup(text); 75 | SDL_free(text); 76 | if (!text_dup) { 77 | LOGW("Could not strdup input text"); 78 | return false; 79 | } 80 | 81 | struct sc_control_msg msg; 82 | msg.type = SC_CONTROL_MSG_TYPE_SET_CLIPBOARD; 83 | msg.set_clipboard.sequence = SC_SEQUENCE_INVALID; 84 | msg.set_clipboard.text = text_dup; 85 | msg.set_clipboard.paste = false; 86 | 87 | if (!sc_controller_push_msg(screen->im.controller, &msg)) { 88 | free(text_dup); 89 | LOGW("Could not request 'set device clipboard'"); 90 | return false; 91 | } 92 | return true; 93 | } 94 | 95 | return sc_screen_handle_event_hijack(screen, event); 96 | } 97 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"}]} -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/SDLUIKitDelegate+Extend.m: -------------------------------------------------------------------------------- 1 | // 2 | // SDLUIKitDelegate+Extend.m 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/11. 6 | // 7 | 8 | #import "SDLUIKitDelegate+Extend.h" 9 | #import "KFKeychain.h" 10 | 11 | @implementation SDLUIKitDelegate (Extend) 12 | 13 | -(void)applicationDidEnterBackground:(UIApplication *)application { 14 | NSTimeInterval beginBackgroundTime = [NSDate date].timeIntervalSince1970; 15 | 16 | // For more time execute in background 17 | static void (^beginTaskHandler)(void) = nil; 18 | beginTaskHandler = ^{ 19 | __block UIBackgroundTaskIdentifier taskIdentifier = [UIApplication.sharedApplication beginBackgroundTaskWithName:@"com.mobile.scrcpy-ios.task" expirationHandler:^{ 20 | [UIApplication.sharedApplication endBackgroundTask:taskIdentifier]; 21 | 22 | if (NSDate.date.timeIntervalSince1970 - beginBackgroundTime < 60 * 2) { 23 | beginTaskHandler(); 24 | } 25 | }]; 26 | }; 27 | beginTaskHandler(); 28 | } 29 | 30 | -(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options 31 | { 32 | NSLog(@"> Received URL: %@", url.absoluteURL); 33 | 34 | // Handle Mode Switch URL 35 | if ([@[ScrcpySwitchModeCommand, ScrcpyModeADB, ScrcpyModeVNC] containsObject:url.host]) { 36 | [self switchScrcpyMode:url]; 37 | return YES; 38 | } 39 | 40 | // Post connect 41 | [NSNotificationCenter.defaultCenter postNotificationName:ScrcpyConnectWithSchemeNotification object:nil userInfo:@{ 42 | ScrcpyConnectWithSchemeURLKey : url 43 | }]; 44 | return YES; 45 | } 46 | 47 | -(void)switchScrcpyMode:(NSURL *)switchURL { 48 | // URL likes scrcpy2://switch?mode=adb|vnc 49 | NSURLComponents *comps = [NSURLComponents componentsWithURL:switchURL resolvingAgainstBaseURL:NO]; 50 | NSArray *modeItems = [comps.queryItems filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"SELF.name == %@", ScrcpySwitchModeKey]]; 51 | if (modeItems.lastObject != nil) { 52 | NSURLQueryItem *modeItem = (NSURLQueryItem *)modeItems.lastObject; 53 | [self saveScrcpyMode:modeItem.value]; 54 | return; 55 | } 56 | 57 | // URL likes scrcpy2://adb|vnc 58 | [self saveScrcpyMode:switchURL.host]; 59 | } 60 | 61 | -(void)saveScrcpyMode:(NSString *)mode { 62 | [KFKeychain saveObject:mode forKey:ScrcpySwitchModeKey]; 63 | NSLog(@"-> Scrcpy switched to %@ mode", mode); 64 | 65 | UIWindow *keyWindow = nil; 66 | for (UIWindow *window in UIApplication.sharedApplication.windows) { 67 | keyWindow = window.isKeyWindow ? window : keyWindow; 68 | } 69 | 70 | NSString *message = [NSString stringWithFormat:@"Scrcpy Remote is Switching to %@ Mode!", [mode uppercaseString]]; 71 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Scrcpy" 72 | message:message 73 | preferredStyle:(UIAlertControllerStyleAlert)]; 74 | [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:(UIAlertActionStyleDefault) handler:^(UIAlertAction * _Nonnull action) { 75 | ScrcpyReloadViewController(keyWindow); 76 | }]]; 77 | 78 | [keyWindow.rootViewController presentViewController:alert animated:YES completion:nil]; 79 | } 80 | 81 | @end 82 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios.xcodeproj/xcshareddata/xcschemes/scrcpy-ios.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 44 | 46 | 52 | 53 | 54 | 55 | 59 | 60 | 61 | 62 | 68 | 70 | 76 | 77 | 78 | 79 | 81 | 82 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/LogsViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // LogsViewController.m 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/7/20. 6 | // 7 | 8 | #import "LogsViewController.h" 9 | #import "LogManager.h" 10 | #import "CVCreate.h" 11 | #import "UICommonUtils.h" 12 | 13 | @interface LogsViewController () 14 | @property (nonatomic, weak) UITextView *logsView; 15 | 16 | @end 17 | 18 | @implementation LogsViewController 19 | 20 | #ifdef DEBUG 21 | +(void)reload { 22 | for (UIWindow *window in UIApplication.sharedApplication.windows) { 23 | if (window.rootViewController.presentedViewController) { 24 | [window.rootViewController.presentedViewController dismissViewControllerAnimated:YES completion:nil]; 25 | } 26 | if ([window.rootViewController isKindOfClass:UINavigationController.class]) { 27 | LogsViewController *vc = [[LogsViewController alloc] initWithNibName:nil bundle:nil]; 28 | UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc]; 29 | [window.rootViewController presentViewController:nav animated:YES completion:nil]; 30 | } 31 | } 32 | } 33 | #endif 34 | 35 | - (void)viewDidLoad { 36 | [super viewDidLoad]; 37 | [self setupViews]; 38 | } 39 | 40 | -(void)setupViews { 41 | self.title = @"Scrcpy Logs"; 42 | 43 | SetupViewControllerAppearance(self); 44 | 45 | // Refresh Logs 46 | UIBarButtonItem *refreshItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"Refresh"] 47 | style:(UIBarButtonItemStylePlain) 48 | target:self 49 | action:@selector(refreshLogs)]; 50 | refreshItem.tintColor = DynamicTintColor(); 51 | UIBarButtonItem *shareItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"Share"] 52 | style:(UIBarButtonItemStylePlain) 53 | target:self 54 | action:@selector(shareLogs)]; 55 | shareItem.tintColor = DynamicTintColor(); 56 | self.navigationItem.rightBarButtonItem = refreshItem; 57 | self.navigationItem.leftBarButtonItem = shareItem; 58 | 59 | CVCreate.UITextView.fontSize(13) 60 | .textColor(DynamicTextColor()) 61 | .text(LogManager.sharedManager.recentLogs) 62 | .addToView(self.view) 63 | .leftAnchor(self.view.leftAnchor, 5) 64 | .rightAnchor(self.view.rightAnchor, -5) 65 | .topAnchor(self.view.topAnchor, 5) 66 | .bottomAnchor(self.view.bottomAnchor, -5) 67 | .customView(^(UITextView *view) { 68 | self.logsView = view; 69 | view.editable = NO; 70 | }); 71 | 72 | [self refreshLogs]; 73 | } 74 | 75 | #pragma mark - Events 76 | 77 | -(void)refreshLogs { 78 | self.logsView.text = LogManager.sharedManager.recentLogs; 79 | [self.logsView scrollRangeToVisible:(NSRange){self.logsView.text.length-1, 1}]; 80 | } 81 | 82 | -(void)shareLogs { 83 | NSArray *shareItems = @[ self.logsView.text ]; 84 | UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:shareItems applicationActivities:nil]; 85 | activityViewController.modalTransitionStyle = UIModalTransitionStyleCoverVertical; 86 | [self presentViewController:activityViewController animated:YES completion:nil]; 87 | } 88 | 89 | @end 90 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "ADB Host" = "ADB 主机地址"; 2 | "ADB Port" = "ADB 主机端口"; 3 | "ADB Port, Default 5555" = "ADB 主机端口,默认 5555"; 4 | "--max-size, Default Unlimited" = "--max-size, 默认无限制"; 5 | "--video-bit-rate, Default 4M" = "--video-bit-rate, 默认 4M"; 6 | "--max-fps, Default 60" = "--max-fps, 默认 60"; 7 | "Turn Screen Off:" = "连接后关闭物理屏幕:"; 8 | "Stay Awake:" = "保持屏幕唤醒:"; 9 | "Force ADB Forward:" = "强制使用 ADB Forward 模式:"; 10 | "Turn Off When Closing:" = "断开连接后关闭屏幕:"; 11 | "Always Show Navigation Buttons:" = "始终显示导航按键:"; 12 | "Enable Audio(Android 11+):" = "开启音频(Android 11 以上):"; 13 | "Power Saving Mode(for iPhone):" = "开启省电模式(iPhone 端):"; 14 | "Connect" = "开始连接"; 15 | "Copy URL Scheme" = "复制当前 URL Scheme"; 16 | "For more help, please visit\nhttps://github.com/wsvn53/scrcpy-mobile" = "获取更多帮助,请访问\nhttps://github.com/wsvn53/scrcpy-mobile"; 17 | "ADB\nConnecting" = "ADB\n连接中"; 18 | "ADB\nConnected" = "ADB\n已连接成功"; 19 | "ADB Connect Failed:\n%@" = "ADB 连接失败:\n%@"; 20 | "Device [%@] connected, but unahtorized. Please accept authorization on your device." = "设备 %@ 已连接,但需要完成授权操作。请在设备后接受授权请求。"; 21 | "Start Scrcpy Failed" = "启动 Scrcpy 失败"; 22 | "Scrcpy\nConnected" = "Scrcpy\n已连接成功"; 23 | "ADB Host is required" = "需要填写 ADB 主机地址"; 24 | "Starting.." = "开始连接.."; 25 | "Copied URL:\n%@" = "已复制 URL:\n%@"; 26 | "Pair with [Pairing Code]" = "用 Pairing Code 匹配"; 27 | "Show detailed scrcpy logs" = "显示 Scrcpy 详细日志"; 28 | "Report an issue" = "报告问题/反馈"; 29 | "Cancel" = "取消"; 30 | "Switch Mode" = "切换连接模式"; 31 | "Switching to VNC Mode?" = "是否切换到 VNC 模式?"; 32 | "Yes, Switch VNC Mode" = "是,切换至 VNC 模式"; 33 | "No, Continue ADB Mode" = "否,继续在 ADB 模式"; 34 | "ADB Pair With Android" = "匹配到 Android 设备"; 35 | "How To Pair With Android Devices:" = "如何匹配 Android 设备:"; 36 | "1. Go to Settings -> System -> Developer Options\n2. Enable Wireless Debugging\n3. Pair device with pairing code" = "1. 前往【设置】->【系统】->【开发者选项】\n2. 开启无线调试\n3. 用匹配码匹配"; 37 | "Note: This feature only available on Android 11 and above!" = "请注意,这个功能只在 Android 11 及以上可用。"; 38 | "ADB Pairing IP Address" = "ADB 设备 IP 地址"; 39 | "ADB Pairing Port" = "ADB 设备匹配端口"; 40 | "ADB Pairing Code" = "ADB 匹配码"; 41 | "Start Pairing" = "开始匹配"; 42 | "Pairing Address is Empty!" = "匹配设备地址不能为空!"; 43 | "Pairing Port is Empty" = "匹配端口不能为空!"; 44 | "Pairing Code is Empty!" = "匹配码不能为空!"; 45 | "Pairing.." = "匹配中.."; 46 | "Scrcpy Remote VNC Client" = "Scrcpy Remote VNC 客户端"; 47 | "VNC Host or ADB Host" = "VNC 地址或者 ADB 设备地址"; 48 | "VNC Port or ADB Port" = "VNC 端口或者 ADB 端口"; 49 | "VNC Password, Optional" = "VNC 密码,选填"; 50 | "Auto Connect:" = "是否自动连接:"; 51 | "View Only:" = "是否仅查看:"; 52 | "Full Screen:" = "全屏模式:"; 53 | "Scrcpy Remote currently only support VNC port over WebSocket. You can setup a websocket port by webcoskify https://github.com/novnc/websockify" = "Scrcpy Remote 当前仅支持基于 WebSocket 的 VNC 端口,你可以通过 websockify 设置一个 WebSocket 代理端口,具体可参考 https://github.com/novnc/websockify"; 54 | "VNC Host is Required" = "VNC 地址不能为空!"; 55 | "VNC Port is Required" = "VNC 端口不能为空!"; 56 | "Switch ADB Mode" = "切换 ADB 模式"; 57 | "Switching to ADB Mode?" = "是否切换到 ADB 模式?"; 58 | "Yes, Switch ADB Mode" = "是,切换到 ADB 模式"; 59 | "No, Continue VNC Mode" = "否,继续在 VNC 模式"; 60 | "Import/Export ADB keys" = "导入/导出 ADB 密钥"; 61 | "Export" = "导出"; 62 | "ADB Private Key(adbkey):" = "ADB 私钥(adbkey):"; 63 | "ADB Public Key(adbkey.pub):" = "ADB 公钥(adbkey.pub):"; 64 | "Save Privatekey & Pubkey" = "保存 adbkey 私钥和公钥"; 65 | "Import ADB Key Pair From File" = "从文件导入 ADB 密钥对"; 66 | "Generate New ADB Key Pair" = "生成新的 ADB 密钥对"; 67 | "Load ADB Key Failed: %@" = "加载 ADB 密钥失败:%@"; 68 | "Load ADB PubKey Failed: %@" = "加载 ADB 公钥失败:%@"; 69 | "New ADB key pair GENERATED.\nPlease RESTART the app for the new key pair to take effect." = "ADB key 生成成功。\n请重启 app 以使新的 ADB Key 生效。"; 70 | "Save [adbkey] ERROR: %@" = "保存 adbkey 错误:%@"; 71 | "Save [adbkey.pub] ERROR: %@" = "保存 adbkey.pub 错误:%@"; 72 | "ADB Key Pairs Saved" = "ADB 密钥对已保存"; 73 | "Please note that after regenerating the ADB key pairs, you may need to RE-AUTHORIZE on your remote phone." = "请注意:生成新的 ADB 密钥对后,你可能需要重新在远程设备上授权连接。"; 74 | "Yes, Continue" = "继续生成"; 75 | "No, Stop Generate" = "取消生成"; 76 | "ADB Key has been MODIFIED but not saved, do you confirm to exit?" = "ADB 密钥对已修改但尚未保存,是否确认退出?"; 77 | "ADB Key Pairs Exported" = "ADB 密钥对已导出"; 78 | "ADB Key Pair Saved! Please restart app to take effect." = "ADB 密钥对保存成功!请重启 app 生效。"; 79 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/SDL_uikitviewcontroller+Extend.m: -------------------------------------------------------------------------------- 1 | // 2 | // SDL_uikitviewcontroller+Extend.m 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/11. 6 | // 7 | 8 | #import "SDL_uikitviewcontroller+Extend.h" 9 | #import 10 | #import "CVCreate.h" 11 | #import "ScrcpyClient.h" 12 | #import "MenubarViewController.h" 13 | 14 | // ScrcpyMenubarGuideDidShow 15 | static NSString *ScrcpyMenubarGuideDidShow = @"ScrcpyMenubarGuideDidShow"; 16 | 17 | @implementation SDL_uikitviewcontroller (Extend) 18 | 19 | // Checked that SDL_uikitviewcontroller not implemented viewDidLoad 20 | -(void)viewDidLoad { 21 | [super viewDidLoad]; 22 | [self performSelector:@selector(addMenubarTriggerView) withObject:nil afterDelay:1.f]; 23 | } 24 | 25 | -(void)showMenubarGuide { 26 | BOOL guideDidShow = [[NSUserDefaults standardUserDefaults] boolForKey:ScrcpyMenubarGuideDidShow]; 27 | if (guideDidShow) { 28 | NSLog(@"Menubar guide has already shown."); 29 | return; 30 | } 31 | 32 | CVCreate.UIStackView(@[ 33 | CVCreate.UIView, 34 | CVCreate.UIStackView(@[ 35 | CVCreate.UIImageView([UIImage imageNamed:@"TouchIcon"]) 36 | .size(CGSizeMake(30, 30)) 37 | .customView(^(UIImageView *view){ 38 | view.contentMode = UIViewContentModeCenter; 39 | }), 40 | CVCreate.UILabel.text(@"Long Press To Show Menu Bar") 41 | .boldFontSize(15.f) 42 | .textAlignment(NSTextAlignmentCenter) 43 | .textColor(UIColor.whiteColor), 44 | ]).spacing(10), 45 | CVCreate.UIView, 46 | ]) 47 | .distribution(UIStackViewDistributionEqualSpacing) 48 | .addToView(self.view) 49 | .click(self, @selector(hideGuideView)) 50 | .border(UIColor.whiteColor, 1.f) 51 | .backgroundColor([UIColor colorWithWhite:1 alpha:0.3]) 52 | .topAnchor(self.view.bottomAnchor, -60) 53 | .bottomAnchor(self.view.bottomAnchor, 0) 54 | .leftAnchor(self.view.leftAnchor, 0) 55 | .rightAnchor(self.view.rightAnchor, 0) 56 | .customView(^(UIView *view){ 57 | view.tag = @"GuideView".hash; 58 | }); 59 | } 60 | 61 | -(void)hideGuideView { 62 | UIView *guideView = [self.view viewWithTag:@"GuideView".hash]; 63 | [UIView animateWithDuration:0.3 animations:^{ 64 | guideView.alpha = 0; 65 | } completion:^(BOOL finished) { 66 | // Mark as shown 67 | [NSUserDefaults.standardUserDefaults setBool:YES forKey:ScrcpyMenubarGuideDidShow]; 68 | 69 | // Remove GuideView and Show Menubar 70 | [guideView removeFromSuperview]; 71 | [self showMenubarView]; 72 | }]; 73 | } 74 | 75 | -(void)addMenubarTriggerView { 76 | if (self.viewLoaded == NO) { 77 | [self performSelector:@selector(addMenubarTriggerView) withObject:nil afterDelay:0.5]; 78 | return; 79 | } 80 | 81 | CVCreate.UIView.addToView(self.view) 82 | .topAnchor(self.view.bottomAnchor, -60) 83 | .bottomAnchor(self.view.bottomAnchor, 0) 84 | .leftAnchor(self.view.leftAnchor, 0) 85 | .rightAnchor(self.view.rightAnchor, 0) 86 | .customView(^(UIView *view) { 87 | UILongPressGestureRecognizer *menuGesture = [[UILongPressGestureRecognizer alloc] 88 | initWithTarget:self 89 | action:@selector(onLongPress:)]; 90 | menuGesture.minimumPressDuration = 0.5f; 91 | [view addGestureRecognizer:menuGesture]; 92 | }); 93 | 94 | // Check Always Show Navigation Buttons Setting 95 | if (ScrcpySharedClient.shouldAlwaysShowNavButtons) { 96 | [self showMenubarView]; 97 | } else { 98 | [self showMenubarGuide]; 99 | } 100 | } 101 | 102 | -(void)onLongPress:(UITapGestureRecognizer *)gesture { 103 | if (gesture.state == UIGestureRecognizerStateBegan) { 104 | [self showMenubarView]; 105 | [self hideGuideView]; 106 | } 107 | } 108 | 109 | -(void)showMenubarView { 110 | MenubarViewController *menuController = [[MenubarViewController alloc] initWithNibName:nil bundle:nil]; 111 | menuController.modalPresentationStyle = UIModalPresentationCustom; 112 | [self presentViewController:menuController animated:YES completion:nil]; 113 | } 114 | 115 | - (void)viewWillLayoutSubviews { 116 | [super viewWillLayoutSubviews]; 117 | for (CALayer *layer in self.view.layer.sublayers) { 118 | if ([layer isKindOfClass:AVSampleBufferDisplayLayer.class]) { 119 | layer.frame = self.view.bounds; 120 | } 121 | } 122 | } 123 | 124 | @end 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scrcpy-mobile 2 | 3 | Ported [scrcpy](https://github.com/Genymobile/scrcpy) for mobile platforms, to remotely control Android devices on your iPhone or Android phone. 4 | 5 | *Currently only supports controlling Android devices from iOS, Android controlling Android devices will be supported in future.* 6 | 7 | ![screenshots](screenshots/screenshots.png) 8 | 9 | ## Features 10 | 11 | * Supports scrcpy with ADB over WiFi ; 12 | * With Hardware decoding, less power and CPU comsumed; 13 | * Optimized gesture experiences for unstable network from mobile devices; 14 | * Supports start scrcpy from URL scheme; 15 | * Supports pair Android device with Pairing Code; 16 | * With Android navigation buttons: Back, Home, Switch App; 17 | * Supports toggle iOS keyboard and send keys to remote device; 18 | * Sync clipboard contents between iPhone and remote Android devices; 19 | * Support adbkey management: generate, modify, import, export; 20 | * Support audio forward to iPhone(for Android 11+ devices); 21 | 22 | ## Installation 23 | 24 | Scrcpy Mobile is now available on the App Store. You can download from: 25 | 26 | [![Get it from iTunes](screenshots/Download_on_the_App_Store_Badge.png)](https://apps.apple.com/us/app/scrcpy-remote/id1629352527) 27 | 28 | ## Usage 29 | 30 | #### ADB Mode: 31 | 32 | After the App is installed, the default mode is VNC. You can switch to ADB WiFi mode by one of the following options: 33 | 34 | - **Option 1**: Visit this URL Scheme by click [scrcpy2://adb](scrcpy2://adb) 35 | - **Option 2**: Type the text `adb` in the **Host** textbox, then click **Connect** 36 | - **Option 3**: Type the text `5555` in the **Port** textbox, then click **Connect** 37 | 38 | And then please make sure that the Android devices has enabled the **adb tcpip** mode: 39 | 40 | ```sh 41 | adb tcpip 5555 42 | ``` 43 | 44 | After authorized on your Android devices, scrcpy will continue to connect. 45 | 46 | **Or Pair with Pairing Code:** 47 | 48 | This only works on Android 10+ devices: 49 | 50 | - Click "..." menu icon on the top-left corner of Scrcpy Remote main window; 51 | - Click "Pair With Pairing Code"; 52 | - Then on the Android device, go to: 53 | - `Settings` -> `System` -> `Developer Options` -> `Enable Wireless Debugging` -> `Pair device with pairing code` 54 | 55 | Then just follow the tips on Android device to start pair. 56 | 57 | #### VNC Mode: 58 | 59 | You can switch back to VNC mode by one of the following options: 60 | 61 | - **Option 1**: Visit the URL Scheme by click [scrcpy2://vnc](scrcpy2://vnc) 62 | - **Option 2**: Type the text `vnc` in the **Host** textbox, then click **Connect** 63 | - **Option 3**: Type the text `5900` in the **Port** textbox, then click **Connect** 64 | 65 | > Note: The VNC mode can only connect the VNC port that be proxied with websockify, and it's based on noVNC which is a web vnc client, so the performance and experience may not good. 66 | 67 | #### URL Scheme: 68 | 69 | After changed the options, you can click "Copy URL Scheme" to get the URL Scheme string, and you can create a shortcut in Shorcuts.app for connecting to scrcpy quickly. 70 | 71 | ``` 72 | scrcpy2://example.com:5555?bit-rate=4M&max-size=1080 73 | ``` 74 | 75 | #### Telegram Support: 76 | 77 | If you still have any question, you can join telegram channel: 78 | 79 | [![telegram](screenshots/telegram.png)](https://t.me/joinchat/I_HBlFpB27RkZTRl) 80 | 81 | ## Build 82 | 83 | Build all dependencies: 84 | 85 | ```sh 86 | make libs 87 | ``` 88 | 89 | Build `scrcpy-server`: 90 | 91 | ```sh 92 | make -C porting scrcpy-server 93 | ``` 94 | 95 | Then, Open `scrcpy-ios/scrcpy-ios.xcodeproj` to Build and Run. 96 | 97 | ## License 98 | 99 | ``` 100 | MIT License 101 | 102 | Copyright (c) 2022 Ethan 103 | 104 | Permission is hereby granted, free of charge, to any person obtaining a copy 105 | of this software and associated documentation files (the "Software"), to deal 106 | in the Software without restriction, including without limitation the rights 107 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 108 | copies of the Software, and to permit persons to whom the Software is 109 | furnished to do so, subject to the following conditions: 110 | 111 | The above copyright notice and this permission notice shall be included in all 112 | copies or substantial portions of the Software. 113 | 114 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 115 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 116 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 117 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 118 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 119 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 120 | SOFTWARE. 121 | ``` 122 | 123 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/UICommonUtils.h: -------------------------------------------------------------------------------- 1 | // 2 | // UICommonUtils.h 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 26/2/23. 6 | // 7 | 8 | #ifndef UICommonUtils_h 9 | #define UICommonUtils_h 10 | 11 | #import 12 | #import "ScrcpySwitch.h" 13 | 14 | #define is_dark_mode (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) 15 | 16 | static inline UIColor *DynamicTintColor(void) { 17 | return [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) { 18 | return is_dark_mode ? UIColor.whiteColor : UIColor.blackColor; 19 | }]; 20 | } 21 | 22 | static inline UIColor *DynamicTextFieldBorderColor(void) { 23 | return [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) { 24 | return is_dark_mode ? UIColor.whiteColor : [UIColor colorWithRed:0 green:0 blue:0 alpha:0.3]; 25 | }]; 26 | } 27 | 28 | static inline UIColor *DynamicTextColor(void) { 29 | return [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) { 30 | return is_dark_mode ? UIColor.whiteColor : UIColor.darkGrayColor; 31 | }]; 32 | } 33 | 34 | static inline UIColor *DynamicBackgroundColor(void) { 35 | return [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) { 36 | return is_dark_mode ? UIColor.darkGrayColor : UIColor.whiteColor; 37 | }]; 38 | } 39 | 40 | static inline NSAttributedString *DynamicColoredPlaceholder(NSString *text) { 41 | UIColor *dynamicColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) { 42 | return is_dark_mode ? UIColor.lightGrayColor : UIColor.grayColor; 43 | }]; 44 | return [[NSAttributedString alloc] initWithString:text attributes:@{ NSForegroundColorAttributeName: dynamicColor}]; 45 | } 46 | 47 | static inline void SetupViewControllerAppearance(UIViewController *vc) { 48 | // Enable dark mode 49 | vc.view.backgroundColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) { 50 | return (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) ? [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1.0] : [UIColor whiteColor]; 51 | }]; 52 | 53 | if (@available(iOS 13.0, *)) { 54 | UINavigationBarAppearance *appearance = [[UINavigationBarAppearance alloc] init]; 55 | [appearance configureWithOpaqueBackground]; 56 | appearance.backgroundColor = [UIColor systemGray6Color]; 57 | vc.navigationController.navigationBar.standardAppearance = appearance; 58 | vc.navigationController.navigationBar.scrollEdgeAppearance = appearance; 59 | } 60 | } 61 | 62 | static inline void ShowAlertFrom(UIViewController *fromController, 63 | NSString *message, 64 | UIAlertAction *okAction, 65 | UIAlertAction *cancelAction) { 66 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Scrcpy Remote" message:message preferredStyle:(UIAlertControllerStyleAlert)]; 67 | 68 | if (okAction) [alert addAction:okAction]; 69 | if (cancelAction) [alert addAction:cancelAction]; 70 | 71 | if (!okAction && !cancelAction) { 72 | [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:(UIAlertActionStyleCancel) handler:nil]]; 73 | } 74 | [fromController presentViewController:alert animated:YES completion:nil]; 75 | } 76 | 77 | static inline CVCreate *CreateDarkButton(NSString *title, id target, SEL action) { 78 | UIColor *borderColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) { 79 | return is_dark_mode ? UIColor.grayColor : UIColor.blackColor; 80 | }]; 81 | return CVCreate.UIButton.text(title) 82 | .boldFontSize(16) 83 | .size(CGSizeMake(0, 45)) 84 | .textColor(UIColor.whiteColor) 85 | .backgroundColor(UIColor.blackColor) 86 | .border(borderColor, 2.f) 87 | .cornerRadius(6) 88 | .click(target, action); 89 | } 90 | 91 | static inline CVCreate *CreateLightButton(NSString *title, id target, SEL action) { 92 | return CVCreate.UIButton.text(title) 93 | .boldFontSize(16) 94 | .size(CGSizeMake(0, 45)) 95 | .textColor(UIColor.blackColor) 96 | .backgroundColor(UIColor.whiteColor) 97 | .border(UIColor.darkGrayColor, 2.f) 98 | .cornerRadius(6) 99 | .click(target, action); 100 | } 101 | 102 | static inline CVCreate *CreateScrcpySwitch(NSString *title, NSString *optionKey, void (^bindBlock)(ScrcpySwitch *view)) { 103 | return CVCreate.UIStackView(@[ 104 | CVCreate.UILabel.text(title) 105 | .fontSize(16.f).textColor([UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) { 106 | return traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark ? UIColor.whiteColor : UIColor.blackColor; 107 | }]), 108 | CVCreate.create(ScrcpySwitch.class) 109 | .customView(^(ScrcpySwitch *view){ 110 | view.optionKey = optionKey; 111 | bindBlock(view); 112 | }), 113 | ]).spacing(10.f); 114 | } 115 | 116 | #endif /* UICommonUtils_h */ 117 | -------------------------------------------------------------------------------- /porting/cmake/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | cmake_minimum_required(VERSION 3.18.1) 3 | 4 | project("scrcpy") 5 | 6 | add_library( # Sets the name of the library. 7 | scrcpy 8 | # Sets the library as a shared library. 9 | STATIC 10 | # Sources of scrcpy 11 | ../../scrcpy/app/src/control_msg.c 12 | 13 | # Replace with porting version of cotnroller.c 14 | # ../../scrcpy/app/src/controller.c 15 | ../../porting/src/controller-porting.c 16 | 17 | ../../scrcpy/app/src/recorder.c 18 | 19 | # av_alloc_context has been moved to demuxer since v2.0 20 | # ../../scrcpy/app/src/demuxer.c 21 | ../../porting/src/demuxer-porting.c 22 | 23 | ../../scrcpy/app/src/cli.c 24 | ../../scrcpy/app/src/receiver.c 25 | ../../scrcpy/app/src/compat.c 26 | ../../scrcpy/app/src/mouse_inject.c 27 | ../../scrcpy/app/src/util/log.c 28 | ../../scrcpy/app/src/util/net_intr.c 29 | ../../scrcpy/app/src/util/thread.c 30 | ../../scrcpy/app/src/util/term.c 31 | ../../scrcpy/app/src/util/process.c 32 | ../../scrcpy/app/src/util/str.c 33 | ../../scrcpy/app/src/util/process_intr.c 34 | ../../scrcpy/app/src/util/strbuf.c 35 | ../../scrcpy/app/src/util/net.c 36 | ../../scrcpy/app/src/util/file.c 37 | ../../scrcpy/app/src/util/acksync.c 38 | ../../scrcpy/app/src/util/tick.c 39 | ../../scrcpy/app/src/util/intmap.c 40 | ../../scrcpy/app/src/util/intr.c 41 | 42 | # Replace with porting version of scrcpy.c 43 | # ../../scrcpy/app/src/scrcpy.c 44 | ../../porting/src/scrcpy-porting.c 45 | 46 | ../../scrcpy/app/src/v4l2_sink.c 47 | 48 | # Replace with porting version of screen.c 49 | # ../../scrcpy/app/src/screen.c 50 | ../../porting/src/screen-porting.c 51 | 52 | ../../scrcpy/app/src/fps_counter.c 53 | ../../scrcpy/app/src/clock.c 54 | ../../scrcpy/app/src/frame_buffer.c 55 | ../../scrcpy/app/src/server.c 56 | ../../scrcpy/app/src/keyboard_inject.c 57 | ../../scrcpy/app/src/file_pusher.c 58 | 59 | # Disable usb sources 60 | # ../../scrcpy/app/src/usb/usb.c 61 | # ../../scrcpy/app/src/usb/aoa_hid.c 62 | # ../../scrcpy/app/src/usb/screen_otg.c 63 | # ../../scrcpy/app/src/usb/hid_mouse.c 64 | # ../../scrcpy/app/src/usb/scrcpy_otg.c 65 | # ../../scrcpy/app/src/usb/hid_keyboard.c 66 | 67 | ../../scrcpy/app/src/adb/adb.c 68 | ../../scrcpy/app/src/adb/adb_device.c 69 | ../../scrcpy/app/src/adb/adb_tunnel.c 70 | ../../scrcpy/app/src/adb/adb_parser.c 71 | 72 | # Remove windows platform sources 73 | # ../../scrcpy/app/src/sys/win/process.c 74 | # ../../scrcpy/app/src/sys/win/file.c 75 | 76 | # Replace process.c to porting version 77 | # ../../scrcpy/app/src/sys/unix/process.c 78 | ../../porting/src/process-porting.c 79 | # Add process manager 80 | ../../porting/src/process-porting.cpp 81 | ../../scrcpy/app/src/sys/unix/file.c 82 | 83 | # Use porting main.c 84 | # ../../scrcpy/app/src/main.c 85 | ../../porting/src/main-porting.c 86 | 87 | ../../scrcpy/app/src/opengl.c 88 | ../../scrcpy/app/src/version.c 89 | 90 | # Replace decoder.c to porting version 91 | # ../../scrcpy/app/src/decoder.c 92 | ../../porting/src/decoder-porting.c 93 | ../../scrcpy/app/src/device_msg.c 94 | ../../scrcpy/app/src/input_manager.c 95 | ../../scrcpy/app/src/icon.c 96 | ../../scrcpy/app/src/options.c 97 | 98 | # Added from v2.0 99 | ../../porting/src/audio_player-porting.c 100 | ../../scrcpy/app/src/delay_buffer.c 101 | ../../scrcpy/app/src/packet_merger.c 102 | ../../scrcpy/app/src/util/memory.c 103 | ../../scrcpy/app/src/util/average.c 104 | ../../scrcpy/app/src/util/bytebuf.c 105 | ../../scrcpy/app/src/util/rand.c 106 | ../../scrcpy/app/src/trait/frame_source.c 107 | ../../scrcpy/app/src/trait/packet_source.c 108 | 109 | # Added from v2.2 110 | ../../porting/src/display-porting.c 111 | ../../scrcpy/app/src/util/timeout.c 112 | ) 113 | 114 | target_include_directories( 115 | scrcpy 116 | PRIVATE 117 | . 118 | ../include 119 | ../../scrcpy/app/src 120 | ../../scrcpy/x/app 121 | ../../output/include 122 | ../../external/adb-mobile/output/include 123 | ../../external/adb-mobile/android-tools/vendor/boringssl/include 124 | ) 125 | 126 | target_compile_options( 127 | scrcpy 128 | PRIVATE 129 | -include porting.h 130 | ) 131 | 132 | target_link_options(scrcpy 133 | PRIVATE 134 | ../../output/iphone/libSDL2.a 135 | ../../output/iphone/libswscale.a 136 | ../../output/iphone/libswresample.a 137 | ../../output/iphone/libavutil.a 138 | ../../output/iphone/libavformat.a 139 | ../../output/iphone/libavfilter.a 140 | ../../output/iphone/libavdevice.a 141 | ../../output/iphone/libavcodec.a 142 | ) 143 | 144 | set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY "") 145 | set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LANGUAGE_STANDARD "c++0x") 146 | 147 | #set_target_properties(scrcpy 148 | # PROPERTIES 149 | # STATIC_LIBRARY_FLAGS "../../output/iphone/libSDL2.a \ 150 | # ../../output/iphone/libswscale.a \ 151 | # ../../output/iphone/libswresample.a \ 152 | # ../../output/iphone/libavutil.a \ 153 | # ../../output/iphone/libavformat.a \ 154 | # ../../output/iphone/libavfilter.a \ 155 | # ../../output/iphone/libavdevice.a \ 156 | # ../../output/iphone/libavcodec.a \ 157 | # ") 158 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/MenubarViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MenubarViewController.m 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/28. 6 | // 7 | 8 | #import "MenubarViewController.h" 9 | #import "CVCreate.h" 10 | #import "ScrcpyClient.h" 11 | 12 | @interface MenubarBackgroundView : UIView 13 | // Proxy touches to target SDL view 14 | @property (nonatomic, weak) UIView *targetSDLView; 15 | 16 | @end 17 | 18 | @implementation MenubarBackgroundView 19 | 20 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 21 | { 22 | for (UITouch *touch in touches) { 23 | if (touch.view != self) return; 24 | } 25 | [self.targetSDLView touchesBegan:touches withEvent:event]; 26 | } 27 | 28 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 29 | { 30 | for (UITouch *touch in touches) { 31 | if (touch.view != self) return; 32 | } 33 | [self.targetSDLView touchesEnded:touches withEvent:event]; 34 | } 35 | 36 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 37 | { 38 | for (UITouch *touch in touches) { 39 | if (touch.view != self) return; 40 | } 41 | [self.targetSDLView touchesCancelled:touches withEvent:event]; 42 | } 43 | 44 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 45 | { 46 | for (UITouch *touch in touches) { 47 | if (touch.view != self) return; 48 | } 49 | [self.targetSDLView touchesMoved:touches withEvent:event]; 50 | } 51 | 52 | @end 53 | 54 | @interface MenubarViewController () 55 | @end 56 | 57 | @implementation MenubarViewController 58 | 59 | - (void)viewDidLoad { 60 | [super viewDidLoad]; 61 | [self setupViews]; 62 | } 63 | 64 | -(void)viewWillLayoutSubviews { 65 | [super viewWillLayoutSubviews]; 66 | NSLog(@"self.view.frame = %@", NSStringFromCGRect(self.view.frame)); 67 | } 68 | 69 | -(void)setupViews { 70 | self.view.backgroundColor = [UIColor clearColor]; 71 | MenubarBackgroundView *backView = [[MenubarBackgroundView alloc] initWithFrame:(CGRectZero)]; 72 | backView.targetSDLView = self.presentingViewController.view; 73 | CVCreate.withView(backView).addToView(self.view) 74 | .widthAnchor(self.view.widthAnchor, 0) 75 | .heightAnchor(self.view.heightAnchor, 0) 76 | .centerXAnchor(self.view.centerXAnchor, 0) 77 | .centerYAnchor(self.view.centerYAnchor, 0) 78 | .click(self, @selector(dismiss:)); 79 | 80 | NSArray *(^CreateMenuItem)(NSString *, NSString *) = ^NSArray *(NSString *iconName, NSString *title) { 81 | return @[ 82 | CVCreate.UIView.size(CGSizeMake(70, 10)), 83 | CVCreate.UIImageView([UIImage imageNamed:iconName]) 84 | .customView(^(UIImageView *view){ 85 | view.contentMode = UIViewContentModeCenter; 86 | }), 87 | CVCreate.UILabel.text(title).fontSize(13) 88 | .textColor(UIColor.whiteColor) 89 | .textAlignment(NSTextAlignmentCenter), 90 | CVCreate.UIView, 91 | ]; 92 | }; 93 | 94 | CVCreate.UIStackView(@[ 95 | CVCreate.UIView, 96 | CVCreate.UIStackView(CreateMenuItem(@"BackIcon", NSLocalizedString(@"Back", nil))).axis(UILayoutConstraintAxisVertical) 97 | .click(self, @selector(sendBackButton:)), 98 | CVCreate.UIView, 99 | CVCreate.UIStackView(CreateMenuItem(@"HomeIcon", NSLocalizedString(@"Home", nil))).axis(UILayoutConstraintAxisVertical) 100 | .click(self, @selector(sendHomeButton:)), 101 | CVCreate.UIView, 102 | CVCreate.UIStackView(CreateMenuItem(@"SwitchAppIcon", @"Switch")).axis(UILayoutConstraintAxisVertical) 103 | .click(self, @selector(sendSwitchAppButton:)), 104 | CVCreate.UIView, 105 | CVCreate.UIStackView(CreateMenuItem(@"KeyboardIcon", @"Keyboard")).axis(UILayoutConstraintAxisVertical) 106 | .click(self, @selector(showKeyboard:)), 107 | CVCreate.UIView, 108 | CVCreate.UIStackView(CreateMenuItem(@"DisconnectIcon", @"Stop")).axis(UILayoutConstraintAxisVertical) 109 | .click(self, @selector(sendDisconnectButton:)), 110 | CVCreate.UIView, 111 | ]).axis(UILayoutConstraintAxisHorizontal) 112 | .distribution(UIStackViewDistributionEqualCentering) 113 | .backgroundColor([UIColor colorWithWhite:0 alpha:0.85]) 114 | .size(CGSizeMake(0, 80)) 115 | .addToView(backView) 116 | .click(self, @selector(doNothing)) 117 | .centerXAnchor(backView.centerXAnchor, 0) 118 | .widthAnchor(backView.widthAnchor, 0) 119 | .bottomAnchor(backView.bottomAnchor, 0); 120 | } 121 | 122 | -(void)doNothing {} 123 | 124 | -(void)dismiss:(UITapGestureRecognizer *)gesture { 125 | SDL_StopTextInput(); 126 | 127 | if (ScrcpySharedClient.shouldAlwaysShowNavButtons) { 128 | NSLog(@"Ignored dismiss, because of shouldAlwaysShowNavButtons has set to YES"); 129 | return; 130 | } 131 | 132 | [self dismissViewControllerAnimated:YES completion:nil]; 133 | } 134 | 135 | -(void)clickAnimated:(UITapGestureRecognizer *)tap { 136 | [UIView animateWithDuration:0.3 delay:0 options:0 animations:^{ 137 | tap.view.backgroundColor = [UIColor colorWithWhite:0.3 alpha:0.5]; 138 | } completion:^(BOOL finished) { 139 | tap.view.backgroundColor = UIColor.clearColor; 140 | }]; 141 | } 142 | 143 | -(void)sendBackButton:(UITapGestureRecognizer *)tap { 144 | [self clickAnimated:tap]; 145 | [ScrcpySharedClient sendBackButton]; 146 | } 147 | 148 | -(void)sendHomeButton:(UITapGestureRecognizer *)tap { 149 | [self clickAnimated:tap]; 150 | [ScrcpySharedClient sendHomeButton]; 151 | } 152 | 153 | -(void)sendSwitchAppButton:(UITapGestureRecognizer *)tap { 154 | [self clickAnimated:tap]; 155 | [ScrcpySharedClient sendSwitchAppButton]; 156 | } 157 | 158 | -(void)showKeyboard:(UITapGestureRecognizer *)tap { 159 | NSLog(@"Showing keyboard"); 160 | SDL_StartTextInput(); 161 | } 162 | 163 | -(void)sendDisconnectButton:(UITapGestureRecognizer *)tap { 164 | [self clickAnimated:tap]; 165 | [ScrcpySharedClient stopScrcpy]; 166 | } 167 | 168 | @end 169 | -------------------------------------------------------------------------------- /porting/src/process-porting.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // process-porting.cpp 3 | // scrcpy-mobile 4 | // 5 | // Created by Ethan on 2022/3/19. 6 | // 7 | 8 | #include "process-porting.hpp" 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | extern "C" { 23 | #include "adb_public.h" 24 | } 25 | 26 | static inline int array_len(const char *arr[]) { 27 | int len = 0; 28 | while (arr[len] != NULL) { len++; } 29 | return len; 30 | } 31 | 32 | /** 33 | * map tp store retured result of pid 34 | */ 35 | 36 | static std::map sc_result_map; 37 | 38 | void sc_store_result(pid_t pid, const char *result) { 39 | sc_result_map.emplace(pid, std::string(result)); 40 | } 41 | 42 | const char *sc_retrieve_result(pid_t pid) { 43 | return sc_result_map[pid].c_str(); 44 | } 45 | 46 | const char *sc_remove_result(int pid) { 47 | std::string result = sc_result_map[pid]; 48 | sc_result_map.erase(pid); 49 | if (result.empty()) { 50 | return strdup(result.c_str()); 51 | } 52 | return NULL; 53 | } 54 | 55 | /** 56 | * map to store return success of pid 57 | */ 58 | static std::map sc_success_map; 59 | 60 | void sc_store_success(pid_t pid, bool success) { 61 | sc_success_map.emplace(pid, success); 62 | } 63 | 64 | bool sc_retrieve_success(pid_t pid) { 65 | return sc_success_map[pid]; 66 | } 67 | 68 | void sc_remove_success(pid_t pid) { 69 | sc_success_map.erase(pid); 70 | } 71 | 72 | /** 73 | * map to store thread of pid 74 | */ 75 | static std::map sc_thread_map; 76 | static std::mutex sc_thread_map_mutex; 77 | 78 | void sc_thread_clean() { 79 | std::lock_guard lock(sc_thread_map_mutex); 80 | std::map pending_clean; 81 | 82 | // try to avoid crash when there is no thread stored 83 | if (sc_thread_map.size() == 0) return; 84 | 85 | for (auto &th : sc_thread_map) { 86 | auto t = th.second; 87 | printf("> check thread %p status %d\n", t, t != nullptr && t->joinable()); 88 | if (t == nullptr) pending_clean[th.first] = th.second; 89 | } 90 | 91 | printf("> cleaning %zu/%zu threads\n", pending_clean.size(), sc_thread_map.size()); 92 | for (auto &th : pending_clean) { 93 | sc_thread_map.erase(th.first); 94 | } 95 | printf("> thread count after clean: %zu\n", sc_thread_map.size()); 96 | } 97 | 98 | void sc_store_thread(pid_t pid, std::thread *thread) { 99 | // clean finished thread 100 | sc_thread_clean(); 101 | 102 | // Store thread 103 | std::lock_guard lock(sc_thread_map_mutex); 104 | sc_thread_map.emplace(pid, thread); 105 | } 106 | 107 | void sc_remove_thread(pid_t pid) { 108 | std::lock_guard lock(sc_thread_map_mutex); 109 | sc_thread_map[pid] = nullptr; 110 | sc_thread_map.erase(pid); 111 | } 112 | 113 | std::thread *sc_retrieve_thread(pid_t pid) { 114 | std::lock_guard lock(sc_thread_map_mutex); 115 | return sc_thread_map[pid]; 116 | } 117 | 118 | void adb_process_thread_func(bool *thread_started, pid_t pid, const char *thread_name, const char *adb_args[]) { 119 | printf("> thread: pid=%d, name=%s started.\n", pid, thread_name); 120 | 121 | // Copy args to local variable 122 | int argc = array_len(adb_args); 123 | const char *argv[argc]; 124 | std::string command = std::string(""); 125 | for (int i = 0; i < argc; i++) { 126 | argv[i] = strdup(adb_args[i]); 127 | char cmd[strlen(argv[i])+2]; 128 | memset(cmd, 0, strlen(argv[i])+2); 129 | sprintf(cmd, " %s", argv[i]); 130 | command.append(cmd); 131 | } 132 | printf("> adb%s\n", command.c_str()); 133 | 134 | // Mark thread_started after copied all arguments 135 | *thread_started = true; 136 | 137 | if (argc > 5 && strcmp(argv[4], "app_process") == 0) { 138 | printf("> scrcpy-server app_process started\n"); 139 | } 140 | 141 | // Change thread name 142 | #ifdef __APPLE__ 143 | pthread_setname_np(thread_name); 144 | #else 145 | pthread_setname_np(pthread_self(), thread_name); 146 | #endif 147 | 148 | // Execute adb command 149 | bool success; 150 | char *result = strdup(""); 151 | 152 | std::thread commandline_thread = std::thread([argc, &argv, &result, &success]() { 153 | int ret_code = adb_commandline_porting(argc, argv, &result); 154 | success = ret_code == 0; 155 | }); 156 | commandline_thread.join(); 157 | 158 | // deal with commandline occur errors and thread exit 159 | if (!success && strlen(result) == 0) { 160 | printf("> commandline_thread failed, save last output\n"); 161 | result = adb_commandline_last_output(); 162 | } 163 | 164 | // Save success 165 | sc_store_success(pid, success); 166 | 167 | // Save result 168 | sc_store_result(pid, result); 169 | 170 | printf("> pid=%d, success=%s\n", pid, success?"true":"false"); 171 | printf("> result:\n%s\n", result?:"(empty)"); 172 | 173 | // Remove from sc_thread_map 174 | sc_thread_map.erase(pid); 175 | sc_thread_map[pid] = nullptr; 176 | } 177 | 178 | int 179 | sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags, 180 | int *pin, int *pout, int *perr) { 181 | // Fake pipe fd 182 | if (pout != nullptr) { 183 | int pipe_fd[2]; 184 | pipe(pipe_fd); 185 | *pout = (sc_pipe)pipe_fd[1]; 186 | } 187 | 188 | // Generate fake pid 189 | *pid = arc4random() % 10000; 190 | 191 | // adb arguments start from index 1 192 | int len = array_len((const char **)argv); 193 | const char *adb_args[len]; 194 | for (int i = 1; i < len; i++) { 195 | adb_args[i-1] = strdup(argv[i]); 196 | } 197 | adb_args[len-1] = NULL; 198 | 199 | // Format thread name 200 | const char *fmt = "ADB-%d"; 201 | int th_len = std::snprintf(nullptr, 0, fmt, *pid); 202 | char th_name[th_len+1]; 203 | std::snprintf(th_name, th_len+1, fmt, *pid); 204 | const char *thread_name = strdup(th_name); 205 | 206 | // Create thread 207 | const char **adb_args_ref = (const char **)adb_args; 208 | bool thread_started = false; 209 | std::thread adb_thread = std::thread([&thread_started, pid, thread_name, adb_args_ref]() { 210 | adb_process_thread_func(&thread_started, *pid, thread_name, adb_args_ref); 211 | }); 212 | sc_store_thread(*pid, &adb_thread); 213 | 214 | // Start thread 215 | adb_thread.detach(); 216 | 217 | // Wait thread start, avoid variable be released 218 | while (thread_started == false) { 219 | usleep(10000); 220 | } 221 | 222 | return 0; 223 | } 224 | 225 | ssize_t 226 | sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, 227 | char *data, size_t len) { 228 | // Wait thread exited to read result 229 | sc_process_wait(pid, false); 230 | const char *result = sc_retrieve_result(pid); 231 | result = result ? : ""; 232 | strcpy(data, result); 233 | return strlen(result) > len ? len : strlen(result); 234 | } 235 | 236 | sc_exit_code 237 | sc_process_wait(pid_t pid, bool close) { 238 | std::thread *adb_thread = sc_retrieve_thread(pid); 239 | if (adb_thread == nullptr) { 240 | return sc_retrieve_success(pid)?0:1; 241 | } 242 | 243 | while((adb_thread = sc_retrieve_thread(pid)) && adb_thread != nullptr) { 244 | usleep(10000); 245 | } 246 | 247 | printf("> wait pid=%d, result=%s\n", pid, sc_retrieve_success(pid)?"true":"false"); 248 | return sc_retrieve_success(pid)?0:1; 249 | } 250 | 251 | bool 252 | sc_process_terminate(pid_t pid) { 253 | std::thread *adb_thread = sc_retrieve_thread(pid); 254 | if (adb_thread == nullptr) { 255 | return true; 256 | } 257 | 258 | printf("> sc_process_terminate, thread pid: %d\n", pid); 259 | sc_remove_thread(pid); 260 | 261 | return true; 262 | } 263 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/PairViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // PairViewController.m 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/7/18. 6 | // 7 | 8 | #import "PairViewController.h" 9 | #import "CVCreate.h" 10 | #import "ScrcpyTextField.h" 11 | #import "ScrcpyClient.h" 12 | #import "MBProgressHUD.h" 13 | #import "UICommonUtils.h" 14 | 15 | @interface PairViewController () 16 | // TextFields 17 | @property (nonatomic, weak) UITextField *pairingAddress; 18 | @property (nonatomic, weak) UITextField *pairingPort; 19 | @property (nonatomic, weak) UITextField *pairingCode; 20 | 21 | @property (nonatomic, weak) UITextField *editingText; 22 | 23 | @end 24 | 25 | @implementation PairViewController 26 | 27 | -(void)loadView { 28 | UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:(CGRectZero)]; 29 | scrollView.alwaysBounceVertical = YES; 30 | self.view = scrollView; 31 | } 32 | 33 | - (void)viewDidLoad { 34 | [super viewDidLoad]; 35 | [self setupViews]; 36 | [self setupEvents]; 37 | } 38 | 39 | -(void)setupViews { 40 | self.title = NSLocalizedString(@"ADB Pair With Android", nil); 41 | 42 | // Setup appearance 43 | SetupViewControllerAppearance(self); 44 | 45 | __weak typeof(self) _self = self; 46 | CVCreate.UIStackView(@[ 47 | CVCreate.UIView.size(CGSizeMake(0, 5)), 48 | CVCreate.UILabel.boldFontSize(15) 49 | .textColor(DynamicTextColor()) 50 | .text(NSLocalizedString(@"How To Pair With Android Devices:", nil)), 51 | CVCreate.UILabel.fontSize(15) 52 | .textColor(DynamicTextColor()) 53 | .customView(^(UILabel *view){ view.numberOfLines = 10; }) 54 | .text(NSLocalizedString(@"1. Go to Settings -> System -> Developer Options\n2. Enable Wireless Debugging\n3. Pair device with pairing code", nil)), 55 | CVCreate.UILabel.boldFontSize(15) 56 | .textColor(DynamicTextColor()) 57 | .text(NSLocalizedString(@"Note: This feature only available on Android 11 and above!", nil)) 58 | .customView(^(UILabel *view){ view.numberOfLines = 2; }), 59 | CVCreate.create(ScrcpyTextField.class).size(CGSizeMake(0, 40)) 60 | .fontSize(16) 61 | .border(DynamicTextFieldBorderColor(), 2.f) 62 | .cornerRadius(5.f) 63 | .customView(^(ScrcpyTextField *view){ 64 | view.attributedPlaceholder = DynamicColoredPlaceholder(NSLocalizedString(@"ADB Pairing IP Address", nil)); 65 | view.autocorrectionType = UITextAutocorrectionTypeNo; 66 | view.autocapitalizationType = UITextAutocapitalizationTypeNone; 67 | if (@available(iOS 13.0, *)) { 68 | view.overrideUserInterfaceStyle = UIUserInterfaceStyleLight; 69 | } 70 | view.delegate = (id)_self; 71 | _self.pairingAddress = view; 72 | }), 73 | CVCreate.create(ScrcpyTextField.class).size(CGSizeMake(0, 40)) 74 | .fontSize(16) 75 | .border(DynamicTextFieldBorderColor(), 2.f) 76 | .cornerRadius(5.f) 77 | .customView(^(ScrcpyTextField *view){ 78 | view.attributedPlaceholder = DynamicColoredPlaceholder(NSLocalizedString(@"ADB Pairing Port", nil)); 79 | view.autocorrectionType = UITextAutocorrectionTypeNo; 80 | view.autocapitalizationType = UITextAutocapitalizationTypeNone; 81 | if (@available(iOS 13.0, *)) { 82 | view.overrideUserInterfaceStyle = UIUserInterfaceStyleLight; 83 | } 84 | view.delegate = (id)_self; 85 | _self.pairingPort = view; 86 | }), 87 | CVCreate.create(ScrcpyTextField.class).size(CGSizeMake(0, 40)) 88 | .fontSize(16) 89 | .border(DynamicTextFieldBorderColor(), 2.f) 90 | .cornerRadius(5.f) 91 | .customView(^(ScrcpyTextField *view){ 92 | view.attributedPlaceholder = DynamicColoredPlaceholder(NSLocalizedString(@"ADB Pairing Code", nil)); 93 | if (@available(iOS 13.0, *)) { 94 | view.overrideUserInterfaceStyle = UIUserInterfaceStyleLight; 95 | } 96 | view.autocorrectionType = UITextAutocorrectionTypeNo; 97 | view.autocapitalizationType = UITextAutocapitalizationTypeNone; 98 | view.delegate = (id)_self; 99 | _self.pairingCode = view; 100 | }), 101 | CVCreate.UIView, 102 | CreateDarkButton(NSLocalizedString(@"Start Pairing", nil), self, @selector(startPairing)), 103 | CreateLightButton(NSLocalizedString(@"Cancel", nil), self, @selector(cancelPairing)), 104 | CVCreate.UIView, 105 | ]).axis(UILayoutConstraintAxisVertical).spacing(15.f) 106 | .addToView(self.view) 107 | .centerXAnchor(self.view.centerXAnchor, 0) 108 | .topAnchor(self.view.topAnchor, 0) 109 | .widthAnchor(self.view.widthAnchor, -30); 110 | } 111 | 112 | -(void)setupEvents { 113 | CVCreate.withView(self.view).click(self, @selector(stopEditing)); 114 | 115 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardDidShow:) 116 | name:UIKeyboardDidShowNotification object:nil]; 117 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardWillHide:) 118 | name:UIKeyboardWillHideNotification object:nil]; 119 | } 120 | 121 | -(void)viewDidLayoutSubviews { 122 | [super viewDidLayoutSubviews]; 123 | UIScrollView *scrollView = (UIScrollView *)self.view; 124 | scrollView.contentSize = self.view.subviews.firstObject.frame.size; 125 | } 126 | 127 | #pragma mark - Events 128 | 129 | -(void)keyboardDidShow:(NSNotification *)notification { 130 | CGRect keyboardRect = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; 131 | NSLog(@"Keyboard Rect: %@", NSStringFromCGRect(keyboardRect)); 132 | 133 | CGRect textFrame = [self.editingText.superview convertRect:self.editingText.frame toView:self.view]; 134 | NSLog(@"Text Rect: %@", NSStringFromCGRect(textFrame)); 135 | CGFloat textOffset = CGRectGetMaxY(textFrame) - keyboardRect.origin.y; 136 | NSLog(@"Text Offset: %@", @(textOffset)); 137 | 138 | if (textOffset <= 0) { 139 | return; 140 | } 141 | 142 | UIScrollView *rootView = (UIScrollView *)self.view; 143 | rootView.contentOffset = (CGPoint){0, textOffset}; 144 | } 145 | 146 | -(void)keyboardWillHide:(NSNotification *)notification { 147 | UIScrollView *rootView = (UIScrollView *)self.view; 148 | [rootView scrollRectToVisible:(CGRect){0, 0, 1, 1} animated:YES]; 149 | } 150 | 151 | -(void)stopEditing { 152 | [self.editingText endEditing:YES]; 153 | [self.pairingAddress endEditing:YES]; 154 | [self.pairingPort endEditing:YES]; 155 | [self.pairingCode endEditing:YES]; 156 | } 157 | 158 | -(void)startPairing { 159 | [self stopEditing]; 160 | 161 | if ([self.pairingAddress.text length] == 0) { 162 | [self showAlert:NSLocalizedString(@"Pairing Address is Empty!", nil)]; 163 | return; 164 | } 165 | 166 | if ([self.pairingAddress.text containsString:@":"] == NO && 167 | [self.pairingPort.text length] == 0) { 168 | [self showAlert:NSLocalizedString(@"Pairing Port is Empty", nil)]; 169 | return; 170 | } 171 | 172 | if ([self.pairingCode.text length] == 0) { 173 | [self showAlert:NSLocalizedString(@"Pairing Code is Empty!", nil)]; 174 | return; 175 | } 176 | 177 | [self showHUDWith:NSLocalizedString(@"Pairing..", nil)]; 178 | 179 | __weak typeof(self) weakSelf = self; 180 | dispatch_async(dispatch_get_global_queue(0, 0), ^{ 181 | NSString *pairingMessage = nil; 182 | 183 | NSString *pairingAddress = self.pairingAddress.text; 184 | if ([pairingAddress containsString:@":"] == NO) { 185 | pairingAddress = [NSString stringWithFormat:@"%@:%@", pairingAddress, self.pairingPort.text]; 186 | } 187 | BOOL success = [ScrcpySharedClient adbExecute:@[ 188 | @"pair", pairingAddress, self.pairingCode.text, 189 | ] message:&pairingMessage]; 190 | 191 | NSLog(@"Result: %@, %@", @(success), pairingMessage); 192 | pairingMessage = [NSString stringWithFormat:@"ADB Pairing\n%@", 193 | pairingMessage]; 194 | 195 | dispatch_async(dispatch_get_main_queue(), ^{ 196 | [MBProgressHUD hideHUDForView:weakSelf.view animated:YES]; 197 | [weakSelf showAlert:pairingMessage]; 198 | }); 199 | }); 200 | } 201 | 202 | -(void)cancelPairing { 203 | [self.navigationController dismissViewControllerAnimated:YES completion:nil]; 204 | } 205 | 206 | -(void)showHUDWith:(NSString *)text { 207 | MBProgressHUD *hud = [MBProgressHUD HUDForView:self.view]; 208 | if (hud == nil) { 209 | hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; 210 | hud.minSize = CGSizeMake(130, 130); 211 | } 212 | hud.label.text = text; 213 | hud.label.numberOfLines = 2; 214 | } 215 | 216 | -(void)showAlert:(NSString *)message { 217 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Scrcpy Remote" message:message preferredStyle:(UIAlertControllerStyleAlert)]; 218 | [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:(UIAlertActionStyleCancel) handler:nil]]; 219 | [self presentViewController:alert animated:YES completion:nil]; 220 | } 221 | 222 | #pragma mark - UITextFieldDelegate 223 | 224 | - (BOOL)textFieldShouldReturn:(UITextField *)textField { 225 | [self stopEditing]; 226 | return NO; 227 | } 228 | 229 | - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField { 230 | self.editingText = textField; 231 | return YES; 232 | } 233 | 234 | @end 235 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/KeysViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // KeysViewController.m 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 27/12/22. 6 | // 7 | 8 | #import "KeysViewController.h" 9 | #import "CVCreate.h" 10 | #import "ScrcpyTextField.h" 11 | #import "ScrcpyClient.h" 12 | #import "MBProgressHUD.h" 13 | #import "UICommonUtils.h" 14 | #import 15 | 16 | int adb_auth_key_generate(const char* filename); 17 | 18 | @interface KeysViewController () 19 | @property (nonatomic, strong) UITextView *keyTextView; 20 | @property (nonatomic, strong) UITextView *pubkeyTextView; 21 | @end 22 | 23 | @implementation KeysViewController 24 | 25 | -(void)loadView { 26 | UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:(CGRectZero)]; 27 | scrollView.alwaysBounceVertical = YES; 28 | self.view = scrollView; 29 | } 30 | 31 | - (void)viewDidLoad { 32 | [super viewDidLoad]; 33 | [self setupViews]; 34 | } 35 | 36 | -(void)setupViews { 37 | self.title = NSLocalizedString(@"Import/Export ADB Keys", nil); 38 | 39 | // Setup appearance 40 | SetupViewControllerAppearance(self); 41 | 42 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Export", nil) 43 | style:(UIBarButtonItemStylePlain) 44 | target:self 45 | action:@selector(onExportADBKey)]; 46 | self.navigationItem.rightBarButtonItem.tintColor = DynamicTintColor(); 47 | 48 | CVCreate.UIStackView(@[ 49 | CVCreate.UIView.size(CGSizeMake(0, 5)), 50 | CVCreate.UILabel.boldFontSize(15) 51 | .text(NSLocalizedString(@"ADB Private Key(adbkey):", nil)) 52 | .textColor(DynamicTextColor()), 53 | CVCreate.withView(self.keyTextView).fontSize(15) 54 | .size((CGSize){0, 200}) 55 | .text([self loadADBKey]) 56 | .cornerRadius(5.f) 57 | .backgroundColor(DynamicBackgroundColor()) 58 | .textColor(DynamicTextColor()) 59 | .border(UIColor.lightGrayColor, 1.f), 60 | CVCreate.UIView.size((CGSize){0, 1}), 61 | CVCreate.UILabel 62 | .boldFontSize(15) 63 | .text(NSLocalizedString(@"ADB Public Key(adbkey.pub):", nil)) 64 | .textColor(DynamicTextColor()), 65 | CVCreate.withView(self.pubkeyTextView).fontSize(15) 66 | .size((CGSize){0, 200}) 67 | .text([self loadADBPubKey]) 68 | .cornerRadius(5.f) 69 | .backgroundColor(DynamicBackgroundColor()) 70 | .textColor(DynamicTextColor()) 71 | .border(DynamicTextFieldBorderColor(), 1.f), 72 | CreateDarkButton(NSLocalizedString(@"Save Privatekey & Pubkey", nil), self, @selector(onSaveADBKeyPair)), 73 | CreateLightButton(NSLocalizedString(@"Import ADB Key Pair From File", nil), self, @selector(onImportADBKeyPair)), 74 | CreateLightButton(NSLocalizedString(@"Generate New ADB Key Pair", nil), self, @selector(onGenerateADBKeyPair)), 75 | CreateLightButton(NSLocalizedString(@"Cancel", nil), self, @selector(cancel)), 76 | ]) 77 | .axis(UILayoutConstraintAxisVertical) 78 | .spacing(15.f) 79 | .addToView(self.view) 80 | .topAnchor(self.view.topAnchor, 0) 81 | .bottomAnchor(self.view.bottomAnchor, 0) 82 | .widthAnchor(self.view.widthAnchor, -30) 83 | .centerXAnchor(self.view.centerXAnchor, 0); 84 | 85 | [self.view addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(endEditing)]]; 86 | } 87 | 88 | #pragma mark - Getters 89 | 90 | -(UITextView *)keyTextView { 91 | return _keyTextView ? : ({ 92 | _keyTextView = [[UITextView alloc] initWithFrame:(CGRectZero)]; 93 | _keyTextView.autocorrectionType = UITextAutocorrectionTypeNo; 94 | _keyTextView.autocapitalizationType = UITextAutocapitalizationTypeNone; 95 | _keyTextView; 96 | }); 97 | } 98 | 99 | -(UITextView *)pubkeyTextView { 100 | return _pubkeyTextView ? : ({ 101 | _pubkeyTextView = [[UITextView alloc] initWithFrame:(CGRectZero)]; 102 | _pubkeyTextView.autocorrectionType = UITextAutocorrectionTypeNo; 103 | _pubkeyTextView.autocapitalizationType = UITextAutocapitalizationTypeNone; 104 | _pubkeyTextView; 105 | }); 106 | } 107 | 108 | #pragma mark - ADB Key Management 109 | 110 | -(NSString *)adbKeyPath { 111 | return [ScrcpyClient.sharedClient.adbHomePath stringByAppendingPathComponent:@".android/adbkey"]; 112 | } 113 | 114 | -(NSString *)adbPubKeyPath { 115 | return [[self adbKeyPath] stringByAppendingString:@".pub"]; 116 | } 117 | 118 | -(NSString *)loadADBKey { 119 | NSError *error = nil; 120 | NSString *adbKey = [NSString stringWithContentsOfFile:[self adbKeyPath] encoding:NSUTF8StringEncoding error:&error]; 121 | if (error != nil) { 122 | ShowAlertFrom(self, [NSString stringWithFormat:NSLocalizedString(@"Load ADB Key Failed: %@", nil), error], nil, nil); 123 | return @""; 124 | } 125 | return adbKey; 126 | } 127 | 128 | -(NSString *)loadADBPubKey { 129 | NSError *error = nil; 130 | NSString *adbKey = [NSString stringWithContentsOfFile:[self adbPubKeyPath] encoding:NSUTF8StringEncoding error:&error]; 131 | if (error != nil) { 132 | ShowAlertFrom(self, [NSString stringWithFormat:NSLocalizedString(@"Load ADB PubKey Failed: %@", nil), error], nil, nil); 133 | return @""; 134 | } 135 | return adbKey; 136 | } 137 | 138 | -(void)saveADBKey { 139 | NSError *saveError = nil; 140 | [self endEditing]; 141 | 142 | if ([[self loadADBKey] isEqualToString:self.keyTextView.text] == NO) { 143 | [self.keyTextView.text writeToFile:[self adbKeyPath] atomically:YES encoding:NSUTF8StringEncoding error:&saveError]; 144 | } 145 | 146 | if (saveError != nil) { 147 | ShowAlertFrom(self, [NSString stringWithFormat:NSLocalizedString(@"Save [adbkey] ERROR: %@", nil), saveError], nil, nil); 148 | return; 149 | } 150 | 151 | if ([[self loadADBPubKey] isEqualToString:self.pubkeyTextView.text] == NO) { 152 | [self.pubkeyTextView.text writeToFile:[self adbPubKeyPath] atomically:YES encoding:NSUTF8StringEncoding error:&saveError]; 153 | } 154 | 155 | if (saveError != nil) { 156 | ShowAlertFrom(self, [NSString stringWithFormat:NSLocalizedString(@"Save [adbkey.pub] ERROR: %@", nil), saveError], nil, nil); 157 | return; 158 | } 159 | 160 | ShowAlertFrom(self, NSLocalizedString(@"ADB Key Pair Saved! Please restart app to take effect.", nil), nil, nil); 161 | } 162 | 163 | -(void)generateNewADBKey { 164 | [MBProgressHUD showHUDAddedTo:self.view animated:YES]; 165 | 166 | __weak typeof(self) weak_self = self; 167 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 168 | adb_auth_key_generate([weak_self adbKeyPath].UTF8String); 169 | dispatch_async(dispatch_get_main_queue(), ^{ 170 | [MBProgressHUD hideHUDForView:weak_self.view animated:YES]; 171 | 172 | // Update adbkey and pubkey 173 | weak_self.keyTextView.text = [self loadADBKey]; 174 | weak_self.pubkeyTextView.text = [self loadADBPubKey]; 175 | 176 | // Required restart app 177 | ShowAlertFrom(weak_self, NSLocalizedString(@"New ADB key pair GENERATED.\nPlease RESTART the app for the new key pair to take effect.", nil), nil, nil); 178 | }); 179 | }); 180 | } 181 | 182 | -(void)importADBKeyFromFiless:(NSArray *)files { 183 | NSLog(@"Importing ADB Key Pair From Files: %@", files); 184 | for (NSURL *file in files) { 185 | BOOL isDir = NO; 186 | [NSFileManager.defaultManager fileExistsAtPath:file.path isDirectory:&isDir]; 187 | if (isDir) { 188 | NSLog(@"-> Ignore, path %@ is directory", file.path); 189 | continue; 190 | } 191 | NSString *content = [NSString stringWithContentsOfFile:file.path encoding:NSUTF8StringEncoding error:nil]; 192 | if ([content containsString:@"BEGIN PRIVATE KEY"]) { 193 | NSLog(@"-> Loading as adbkey: %@", file.path); 194 | self.keyTextView.text = content; 195 | } 196 | 197 | if ([file.pathExtension isEqualToString:@"pub"]) { 198 | NSLog(@"-> Loading as adbkey.pub: %@", file.path); 199 | self.pubkeyTextView.text = content; 200 | } 201 | 202 | [self saveADBKey]; 203 | } 204 | } 205 | 206 | #pragma mark - Actions 207 | 208 | -(void)onSaveADBKeyPair { 209 | NSLog(@"Saving ADB Key"); 210 | [self saveADBKey]; 211 | } 212 | 213 | -(void)onGenerateADBKeyPair { 214 | NSLog(@"Generating New ADB Key"); 215 | 216 | ShowAlertFrom(self, NSLocalizedString(@"Please note that after regenerating the ADB key pair, you may need to RE-AUTHORIZE on your remote phone.", nil), [UIAlertAction actionWithTitle:NSLocalizedString(@"Yes, Continue", nil) style:(UIAlertActionStyleDefault) handler:^(UIAlertAction * _Nonnull action) { 217 | [self generateNewADBKey]; 218 | }], [UIAlertAction actionWithTitle:NSLocalizedString(@"No, Stop Generate", nil) style:(UIAlertActionStyleCancel) handler:nil]); 219 | } 220 | 221 | -(void)cancel { 222 | NSLog(@"Cancel and Exit ADB Key Page"); 223 | 224 | // Check if the key has been modified 225 | if ([[self loadADBKey] isEqualToString:self.keyTextView.text]) { 226 | [self dismissViewControllerAnimated:YES completion:nil]; 227 | return; 228 | } 229 | 230 | // ADB Key Changed, Confirm to Exit 231 | ShowAlertFrom(self, NSLocalizedString(@"ADB Key has been MODIFIED but not saved, do you confirm to exit?", nil), [UIAlertAction actionWithTitle:@"OK" style:(UIAlertActionStyleDefault) handler:^(UIAlertAction * _Nonnull action) { 232 | [self dismissViewControllerAnimated:YES completion:nil]; 233 | }], [UIAlertAction actionWithTitle:@"Cancel" style:(UIAlertActionStyleCancel) handler:nil]); 234 | } 235 | 236 | -(void)endEditing { 237 | [self.keyTextView endEditing:YES]; 238 | [self.pubkeyTextView endEditing:YES]; 239 | } 240 | 241 | -(void)onExportADBKey { 242 | // Export ADB Key to Local File 243 | UIDocumentPickerViewController *pickerController = [[UIDocumentPickerViewController alloc] initWithURLs:@[ 244 | [NSURL fileURLWithPath:[self adbKeyPath]], 245 | [NSURL fileURLWithPath:[self adbPubKeyPath]], 246 | ] inMode:(UIDocumentPickerModeExportToService)]; 247 | pickerController.delegate = (id)self; 248 | [self presentViewController:pickerController animated:YES completion:nil]; 249 | } 250 | 251 | -(void)onImportADBKeyPair { 252 | NSLog(@"Importing ADB Key Pair"); 253 | UIDocumentPickerViewController *importController = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[ @"public.item" ] inMode:(UIDocumentPickerModeImport)]; 254 | importController.allowsMultipleSelection = YES; 255 | importController.delegate = (id)self; 256 | [self presentViewController:importController animated:YES completion:nil]; 257 | } 258 | 259 | #pragma mark - UIDocumentPickerDelegate 260 | 261 | - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls { 262 | NSLog(@"Picked URLs: %@", urls); 263 | 264 | if (controller.documentPickerMode == UIDocumentPickerModeExportToService) { 265 | MBProgressHUD *tipsView = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; 266 | tipsView.mode = MBProgressHUDModeText; 267 | tipsView.label.text = NSLocalizedString(@"ADB Key Pair Exported", nil); 268 | 269 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 270 | [tipsView hideAnimated:YES]; 271 | }); 272 | } else if (controller.documentPickerMode == UIDocumentPickerModeImport) { 273 | [self importADBKeyFromFiless:urls]; 274 | } 275 | } 276 | 277 | @end 278 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-vnc/VNCViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // VNCViewController.m 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/15. 6 | // 7 | 8 | #import "VNCViewController.h" 9 | #import "HTTPServer.h" 10 | #import "VNCHTTPConnection.h" 11 | #import "CVCreate.h" 12 | #import "VNCBrowserViewController.h" 13 | 14 | #import "ScrcpyTextField.h" 15 | #import "ScrcpySwitch.h" 16 | 17 | @interface VNCViewController () 18 | 19 | @property (nonatomic, strong) NSOperationQueue *httpQueue; 20 | @property (nonatomic, strong) HTTPServer *httpServer; 21 | 22 | @property (nonatomic, weak) ScrcpyTextField *vncHost; 23 | @property (nonatomic, weak) ScrcpyTextField *vncPort; 24 | @property (nonatomic, weak) ScrcpyTextField *vncPassword; 25 | 26 | @property (nonatomic, weak) ScrcpySwitch *autoConnect; 27 | @property (nonatomic, weak) ScrcpySwitch *viewOnly; 28 | @property (nonatomic, weak) ScrcpySwitch *fullScreen; 29 | 30 | @property (nonatomic, weak) UITextField *editingText; 31 | 32 | @end 33 | 34 | @implementation VNCViewController 35 | 36 | -(void)loadView { 37 | UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:(CGRectZero)]; 38 | scrollView.alwaysBounceVertical = YES; 39 | self.view = scrollView; 40 | } 41 | 42 | - (void)viewDidLoad { 43 | [super viewDidLoad]; 44 | [self setupViews]; 45 | [self setupEvents]; 46 | [self startWebServer]; 47 | } 48 | 49 | -(void)viewDidAppear:(BOOL)animated { 50 | [super viewDidAppear:animated]; 51 | [self.navigationController setNavigationBarHidden:NO animated:YES]; 52 | } 53 | 54 | -(void)setupEvents { 55 | CVCreate.withView(self.view).click(self, @selector(stopEditing)); 56 | 57 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardDidShow:) 58 | name:UIKeyboardDidShowNotification object:nil]; 59 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardWillHide:) 60 | name:UIKeyboardWillHideNotification object:nil]; 61 | } 62 | 63 | -(void)setupViews { 64 | self.title = NSLocalizedString(@"Scrcpy Remote VNC Client", nil); 65 | if (@available(iOS 13.0, *)) { 66 | UINavigationBarAppearance *appearance = [[UINavigationBarAppearance alloc] init]; 67 | [appearance configureWithOpaqueBackground]; 68 | appearance.backgroundColor = [UIColor systemGray6Color]; 69 | self.navigationController.navigationBar.standardAppearance = appearance; 70 | self.navigationController.navigationBar.scrollEdgeAppearance = appearance; 71 | } 72 | self.view.backgroundColor = UIColor.whiteColor; 73 | 74 | __weak typeof(self) _self = self; 75 | CVCreate.UIStackView(@[ 76 | CVCreate.UIView, 77 | CVCreate.create(ScrcpyTextField.class).size(CGSizeMake(0, 40)) 78 | .fontSize(16) 79 | .border([UIColor colorWithRed:0 green:0 blue:0 alpha:0.3], 2.f) 80 | .cornerRadius(5.f) 81 | .customView(^(ScrcpyTextField *view){ 82 | view.optionKey = @"vnc-host"; 83 | view.placeholder = NSLocalizedString(@"VNC Host or ADB Host", nil); 84 | view.autocorrectionType = UITextAutocorrectionTypeNo; 85 | view.autocapitalizationType = UITextAutocapitalizationTypeNone; 86 | if (@available(iOS 13.0, *)) { 87 | view.overrideUserInterfaceStyle = UIUserInterfaceStyleLight; 88 | } 89 | view.delegate = (id)_self; 90 | _self.vncHost = view; 91 | }), 92 | CVCreate.create(ScrcpyTextField.class).size(CGSizeMake(0, 40)) 93 | .fontSize(16) 94 | .border([UIColor colorWithRed:0 green:0 blue:0 alpha:0.3], 2.f) 95 | .cornerRadius(5.f) 96 | .customView(^(ScrcpyTextField *view){ 97 | view.optionKey = @"vnc-port"; 98 | view.placeholder = NSLocalizedString(@"VNC Port or ADB Port", nil); 99 | if (@available(iOS 13.0, *)) { 100 | view.overrideUserInterfaceStyle = UIUserInterfaceStyleLight; 101 | } 102 | view.autocorrectionType = UITextAutocorrectionTypeNo; 103 | view.autocapitalizationType = UITextAutocapitalizationTypeNone; 104 | view.delegate = (id)_self; 105 | _self.vncPort = view; 106 | }), 107 | CVCreate.create(ScrcpyTextField.class).size(CGSizeMake(0, 40)) 108 | .fontSize(16) 109 | .border([UIColor colorWithRed:0 green:0 blue:0 alpha:0.3], 2.f) 110 | .cornerRadius(5.f) 111 | .customView(^(ScrcpyTextField *view){ 112 | view.optionKey = @"vnc-password"; 113 | view.placeholder = NSLocalizedString(@"VNC Password, Optional", nil); 114 | if (@available(iOS 13.0, *)) { 115 | view.overrideUserInterfaceStyle = UIUserInterfaceStyleLight; 116 | } 117 | view.autocorrectionType = UITextAutocorrectionTypeNo; 118 | view.autocapitalizationType = UITextAutocapitalizationTypeNone; 119 | view.delegate = (id)_self; 120 | view.secureTextEntry = YES; 121 | _self.vncPassword = view; 122 | }), 123 | CVCreate.UIStackView(@[ 124 | CVCreate.UILabel.text(NSLocalizedString(@"Auto Connect:", nil)) 125 | .fontSize(16.f).textColor(UIColor.blackColor), 126 | CVCreate.create(ScrcpySwitch.class) 127 | .customView(^(ScrcpySwitch *view){ 128 | view.optionKey = @"vnc-auto-connect"; 129 | _self.autoConnect = view; 130 | }), 131 | ]).spacing(10.f), 132 | CVCreate.UIStackView(@[ 133 | CVCreate.UILabel.text(NSLocalizedString(@"View Only:", nil)) 134 | .fontSize(16.f).textColor(UIColor.blackColor), 135 | CVCreate.create(ScrcpySwitch.class) 136 | .customView(^(ScrcpySwitch *view){ 137 | view.optionKey = @"vnc-view-only"; 138 | _self.viewOnly = view; 139 | }), 140 | ]).spacing(10.f), 141 | CVCreate.UIStackView(@[ 142 | CVCreate.UILabel.text(NSLocalizedString(@"Full Screen:", nil)) 143 | .fontSize(16.f).textColor(UIColor.blackColor), 144 | CVCreate.create(ScrcpySwitch.class) 145 | .customView(^(ScrcpySwitch *view){ 146 | view.optionKey = @"vnc-full-screen"; 147 | _self.fullScreen = view; 148 | }), 149 | ]).spacing(10.f), 150 | CVCreate.UIButton.backgroundColor(UIColor.blackColor) 151 | .cornerRadius(5.f).text(NSLocalizedString(@"Connect", nil)) 152 | .click(self, @selector(startVNCBrowser)) 153 | .size(CGSizeMake(180, 40)), 154 | CVCreate.UILabel.text(NSLocalizedString(@"Scrcpy Remote currently only support VNC port over WebSocket. You can setup a websocket port by webcoskify https://github.com/novnc/websockify", nil)) 155 | .click(self, @selector(openWebsockify)) 156 | .textColor(UIColor.grayColor) 157 | .fontSize(15.f) 158 | .textAlignment(NSTextAlignmentCenter) 159 | .customView(^(UILabel *view){ 160 | view.numberOfLines = 5; 161 | }), 162 | CVCreate.UIView, 163 | ]).axis(UILayoutConstraintAxisVertical) 164 | .spacing(15.f) 165 | .distribution(UIStackViewDistributionFill) 166 | .addToView(self.view) 167 | .widthAnchor(self.view.widthAnchor, -30) 168 | .centerXAnchor(self.view.centerXAnchor, 0) 169 | .topAnchor(self.view.topAnchor, 20); 170 | } 171 | 172 | -(void)startWebServer { 173 | self.httpServer = [[HTTPServer alloc] init]; 174 | self.httpServer.connectionClass = VNCHTTPConnection.class; 175 | 176 | [self.httpServer setType:@"_http._tcp."]; 177 | [self.httpServer setPort:25900]; 178 | 179 | NSString *webPath = [[NSBundle mainBundle] bundlePath]; 180 | [self.httpServer setDocumentRoot:webPath]; 181 | 182 | __weak typeof(self) weakSelf = self; 183 | NSLog(@"-> Starting HTTPServer at :%d", self.httpServer.port); 184 | self.httpQueue = [[NSOperationQueue alloc] init]; 185 | self.httpQueue.maxConcurrentOperationCount = 1; 186 | [self.httpQueue addOperationWithBlock:^{ 187 | NSError *error; 188 | if(![weakSelf.httpServer start:&error]) { 189 | NSLog(@"Error starting HTTP Server: %@", error); 190 | } 191 | }]; 192 | } 193 | 194 | -(void)showAlert:(NSString *)message { 195 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Scrcpy Remote" message:message preferredStyle:(UIAlertControllerStyleAlert)]; 196 | [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:(UIAlertActionStyleCancel) handler:nil]]; 197 | [self presentViewController:alert animated:YES completion:nil]; 198 | } 199 | 200 | -(void)startVNCBrowser { 201 | [self stopEditing]; 202 | 203 | [self.vncHost updateOptionValue]; 204 | [self.vncPort updateOptionValue]; 205 | [self.vncPassword updateOptionValue]; 206 | [self.autoConnect updateOptionValue]; 207 | [self.viewOnly updateOptionValue]; 208 | [self.fullScreen updateOptionValue]; 209 | 210 | // Auto check HOST and PORT to switch adb mode 211 | if ([self.vncHost.text isEqualToString:@"adb"] || 212 | [self.vncPort.text isEqualToString:@"5555"]) { 213 | __weak typeof(self) weakSelf = self; 214 | [self switchADBMode:^{ 215 | [weakSelf finalStartVNCBrowser]; 216 | }]; 217 | return; 218 | } 219 | 220 | // Final 221 | [self finalStartVNCBrowser]; 222 | } 223 | 224 | -(void)finalStartVNCBrowser { 225 | if (self.vncHost.text.length == 0) { 226 | [self showAlert:NSLocalizedString(@"VNC Host is Required", nil)]; 227 | return; 228 | } 229 | 230 | if (self.vncPort.text.length == 0) { 231 | [self showAlert:NSLocalizedString(@"VNC Port is Required", nil)]; 232 | return; 233 | } 234 | 235 | NSURLComponents *vncComps = [NSURLComponents componentsWithString:@"http://127.0.0.1:25900/vnc.html"]; 236 | vncComps.queryItems = [NSArray array]; 237 | vncComps.queryItems = [vncComps.queryItems arrayByAddingObject:[NSURLQueryItem queryItemWithName:@"host" value:self.vncHost.text]]; 238 | vncComps.queryItems = [vncComps.queryItems arrayByAddingObject:[NSURLQueryItem queryItemWithName:@"port" value:self.vncPort.text]]; 239 | 240 | if (self.vncPassword.text.length > 0) { 241 | vncComps.queryItems = [vncComps.queryItems arrayByAddingObject:[NSURLQueryItem queryItemWithName:@"password" value:self.vncPassword.text]]; 242 | } 243 | 244 | if (self.autoConnect.on) { 245 | vncComps.queryItems = [vncComps.queryItems arrayByAddingObject:[NSURLQueryItem queryItemWithName:@"autoconnect" value:@"1"]]; 246 | } 247 | 248 | if (self.viewOnly.on) { 249 | vncComps.queryItems = [vncComps.queryItems arrayByAddingObject:[NSURLQueryItem queryItemWithName:@"view_only" value:@"1"]]; 250 | } 251 | 252 | VNCBrowserViewController *browserController = [[VNCBrowserViewController alloc] initWithNibName:nil bundle:nil]; 253 | browserController.showsFullscreen = self.fullScreen.on; 254 | browserController.vncURL = [vncComps URL].absoluteString; 255 | browserController.title = [NSString stringWithFormat:@"Remote(%@)", self.vncHost.text]; 256 | [self.navigationController pushViewController:browserController animated:YES]; 257 | } 258 | 259 | -(void)openWebsockify { 260 | [UIApplication.sharedApplication openURL:[NSURL URLWithString:@"https://github.com/novnc/websockify"] 261 | options:@{} 262 | completionHandler:nil]; 263 | } 264 | 265 | -(void)keyboardDidShow:(NSNotification *)notification { 266 | CGRect keyboardRect = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; 267 | NSLog(@"Keyboard Rect: %@", NSStringFromCGRect(keyboardRect)); 268 | 269 | CGRect textFrame = [self.editingText.superview convertRect:self.editingText.frame toView:self.view]; 270 | NSLog(@"Text Rect: %@", NSStringFromCGRect(textFrame)); 271 | CGFloat textOffset = CGRectGetMaxY(textFrame) - keyboardRect.origin.y; 272 | NSLog(@"Text Offset: %@", @(textOffset)); 273 | 274 | if (textOffset <= 0) { 275 | return; 276 | } 277 | 278 | UIScrollView *rootView = (UIScrollView *)self.view; 279 | rootView.contentOffset = (CGPoint){0, textOffset}; 280 | } 281 | 282 | -(void)keyboardWillHide:(NSNotification *)notification { 283 | UIScrollView *rootView = (UIScrollView *)self.view; 284 | [rootView scrollRectToVisible:(CGRect){0, 0, 1, 1} animated:YES]; 285 | } 286 | 287 | -(void)stopEditing { 288 | [self.vncHost endEditing:YES]; 289 | [self.vncPort endEditing:YES]; 290 | [self.vncPassword endEditing:YES]; 291 | } 292 | 293 | -(void)switchADBMode:(void(^)(void))continueCompletion { 294 | UIAlertController *switchController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Switch ADB Mode", nil) message:NSLocalizedString(@"Switching to ADB Mode?", nil) preferredStyle:UIAlertControllerStyleAlert]; 295 | [switchController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Yes, Switch ADB Mode", nil) style:(UIAlertActionStyleDefault) handler:^(UIAlertAction * _Nonnull action) { 296 | // Switch to ADB mode 297 | NSURL *adbURL = [NSURL URLWithString:@"scrcpy2://adb"]; 298 | [UIApplication.sharedApplication openURL:adbURL options:@{} completionHandler:nil]; 299 | }]]; 300 | [switchController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"No, Continue VNC Mode", nil) style:(UIAlertActionStyleCancel) handler:^(UIAlertAction * _Nonnull action) { 301 | continueCompletion(); 302 | }]]; 303 | 304 | [self presentViewController:switchController animated:YES completion:nil]; 305 | } 306 | 307 | #pragma mark - UITextFieldDelegate 308 | 309 | - (BOOL)textFieldShouldReturn:(UITextField *)textField { 310 | [self stopEditing]; 311 | return NO; 312 | } 313 | 314 | - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField { 315 | self.editingText = textField; 316 | return YES; 317 | } 318 | 319 | @end 320 | -------------------------------------------------------------------------------- /porting/server/src/main/java/com/genymobile/scrcpy/Device.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import com.genymobile.scrcpy.wrappers.ClipboardManager; 4 | import com.genymobile.scrcpy.wrappers.DisplayControl; 5 | import com.genymobile.scrcpy.wrappers.InputManager; 6 | import com.genymobile.scrcpy.wrappers.ServiceManager; 7 | import com.genymobile.scrcpy.wrappers.SurfaceControl; 8 | import com.genymobile.scrcpy.wrappers.WindowManager; 9 | 10 | import android.content.IOnPrimaryClipChangedListener; 11 | import android.graphics.Rect; 12 | import android.os.Build; 13 | import android.os.IBinder; 14 | import android.os.SystemClock; 15 | import android.view.IDisplayFoldListener; 16 | import android.view.IRotationWatcher; 17 | import android.view.InputDevice; 18 | import android.view.InputEvent; 19 | import android.view.KeyCharacterMap; 20 | import android.view.KeyEvent; 21 | 22 | import java.util.concurrent.atomic.AtomicBoolean; 23 | 24 | public final class Device { 25 | 26 | public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; 27 | public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL; 28 | 29 | public static final int INJECT_MODE_ASYNC = InputManager.INJECT_INPUT_EVENT_MODE_ASYNC; 30 | public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT; 31 | public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH; 32 | 33 | public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; 34 | public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; 35 | 36 | public interface RotationListener { 37 | void onRotationChanged(int rotation); 38 | } 39 | 40 | public interface FoldListener { 41 | void onFoldChanged(int displayId, boolean folded); 42 | } 43 | 44 | public interface ClipboardListener { 45 | void onClipboardTextChanged(String text); 46 | } 47 | 48 | private final Size deviceSize; 49 | private final Rect crop; 50 | private int maxSize; 51 | private final int lockVideoOrientation; 52 | 53 | private ScreenInfo screenInfo; 54 | public RotationListener rotationListener; 55 | private FoldListener foldListener; 56 | private ClipboardListener clipboardListener; 57 | private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); 58 | 59 | /** 60 | * Logical display identifier 61 | */ 62 | private final int displayId; 63 | 64 | /** 65 | * The surface flinger layer stack associated with this logical display 66 | */ 67 | private final int layerStack; 68 | 69 | private final boolean supportsInputEvents; 70 | 71 | public Device(Options options) throws ConfigurationException { 72 | displayId = options.getDisplayId(); 73 | DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); 74 | if (displayInfo == null) { 75 | Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); 76 | throw new ConfigurationException("Unknown display id: " + displayId); 77 | } 78 | 79 | int displayInfoFlags = displayInfo.getFlags(); 80 | 81 | deviceSize = displayInfo.getSize(); 82 | crop = options.getCrop(); 83 | maxSize = options.getMaxSize(); 84 | lockVideoOrientation = options.getLockVideoOrientation(); 85 | 86 | screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); 87 | layerStack = displayInfo.getLayerStack(); 88 | 89 | ServiceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { 90 | @Override 91 | public void onRotationChanged(int rotation) { 92 | synchronized (Device.this) { 93 | screenInfo = screenInfo.withDeviceRotation(rotation); 94 | 95 | // notify 96 | if (rotationListener != null) { 97 | rotationListener.onRotationChanged(rotation); 98 | } 99 | } 100 | } 101 | }, displayId); 102 | 103 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 104 | ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() { 105 | @Override 106 | public void onDisplayFoldChanged(int displayId, boolean folded) { 107 | if (Device.this.displayId != displayId) { 108 | // Ignore events related to other display ids 109 | return; 110 | } 111 | 112 | synchronized (Device.this) { 113 | DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); 114 | if (displayInfo == null) { 115 | Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); 116 | return; 117 | } 118 | 119 | screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), options.getCrop(), 120 | options.getMaxSize(), options.getLockVideoOrientation()); 121 | // notify 122 | if (foldListener != null) { 123 | foldListener.onFoldChanged(displayId, folded); 124 | } 125 | } 126 | } 127 | }); 128 | } 129 | 130 | if (options.getControl() && options.getClipboardAutosync()) { 131 | // If control and autosync are enabled, synchronize Android clipboard to the computer automatically 132 | ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); 133 | if (clipboardManager != null) { 134 | clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { 135 | @Override 136 | public void dispatchPrimaryClipChanged() { 137 | if (isSettingClipboard.get()) { 138 | // This is a notification for the change we are currently applying, ignore it 139 | return; 140 | } 141 | synchronized (Device.this) { 142 | if (clipboardListener != null) { 143 | String text = getClipboardText(); 144 | if (text != null) { 145 | clipboardListener.onClipboardTextChanged(text); 146 | } 147 | } 148 | } 149 | } 150 | }); 151 | } else { 152 | Ln.w("No clipboard manager, copy-paste between device and computer will not work"); 153 | } 154 | } 155 | 156 | if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { 157 | Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); 158 | } 159 | 160 | // main display or any display on Android >= Q 161 | supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; 162 | if (!supportsInputEvents) { 163 | Ln.w("Input events are not supported for secondary displays before Android 10"); 164 | } 165 | } 166 | 167 | public synchronized void setMaxSize(int newMaxSize) { 168 | maxSize = newMaxSize; 169 | screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); 170 | } 171 | 172 | public synchronized ScreenInfo getScreenInfo() { 173 | return screenInfo; 174 | } 175 | 176 | public int getLayerStack() { 177 | return layerStack; 178 | } 179 | 180 | public Point getPhysicalPoint(Position position) { 181 | // it hides the field on purpose, to read it with a lock 182 | @SuppressWarnings("checkstyle:HiddenField") 183 | ScreenInfo screenInfo = getScreenInfo(); // read with synchronization 184 | 185 | // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation 186 | Size unlockedVideoSize = screenInfo.getUnlockedVideoSize(); 187 | 188 | int reverseVideoRotation = screenInfo.getReverseVideoRotation(); 189 | // reverse the video rotation to apply the events 190 | Position devicePosition = position.rotate(reverseVideoRotation); 191 | 192 | Size clientVideoSize = devicePosition.getScreenSize(); 193 | if (!unlockedVideoSize.equals(clientVideoSize)) { 194 | // The client sends a click relative to a video with wrong dimensions, 195 | // the device may have been rotated since the event was generated, so ignore the event 196 | return null; 197 | } 198 | Rect contentRect = screenInfo.getContentRect(); 199 | Point point = devicePosition.getPoint(); 200 | int convertedX = contentRect.left + point.getX() * contentRect.width() / unlockedVideoSize.getWidth(); 201 | int convertedY = contentRect.top + point.getY() * contentRect.height() / unlockedVideoSize.getHeight(); 202 | return new Point(convertedX, convertedY); 203 | } 204 | 205 | public static String getDeviceName() { 206 | return Build.MODEL; 207 | } 208 | 209 | public static boolean supportsInputEvents(int displayId) { 210 | return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; 211 | } 212 | 213 | public boolean supportsInputEvents() { 214 | return supportsInputEvents; 215 | } 216 | 217 | public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) { 218 | if (!supportsInputEvents(displayId)) { 219 | throw new AssertionError("Could not inject input event if !supportsInputEvents()"); 220 | } 221 | 222 | if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) { 223 | return false; 224 | } 225 | 226 | return ServiceManager.getInputManager().injectInputEvent(inputEvent, injectMode); 227 | } 228 | 229 | public boolean injectEvent(InputEvent event, int injectMode) { 230 | return injectEvent(event, displayId, injectMode); 231 | } 232 | 233 | public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) { 234 | long now = SystemClock.uptimeMillis(); 235 | KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, 236 | InputDevice.SOURCE_KEYBOARD); 237 | return injectEvent(event, displayId, injectMode); 238 | } 239 | 240 | public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) { 241 | return injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode); 242 | } 243 | 244 | public static boolean pressReleaseKeycode(int keyCode, int displayId, int injectMode) { 245 | return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId, injectMode) 246 | && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode); 247 | } 248 | 249 | public boolean pressReleaseKeycode(int keyCode, int injectMode) { 250 | return pressReleaseKeycode(keyCode, displayId, injectMode); 251 | } 252 | 253 | public static boolean isScreenOn() { 254 | return ServiceManager.getPowerManager().isScreenOn(); 255 | } 256 | 257 | public synchronized void setRotationListener(RotationListener rotationListener) { 258 | this.rotationListener = rotationListener; 259 | } 260 | 261 | public synchronized void setFoldListener(FoldListener foldlistener) { 262 | this.foldListener = foldlistener; 263 | } 264 | 265 | public synchronized void setClipboardListener(ClipboardListener clipboardListener) { 266 | this.clipboardListener = clipboardListener; 267 | } 268 | 269 | public static void expandNotificationPanel() { 270 | ServiceManager.getStatusBarManager().expandNotificationsPanel(); 271 | } 272 | 273 | public static void expandSettingsPanel() { 274 | ServiceManager.getStatusBarManager().expandSettingsPanel(); 275 | } 276 | 277 | public static void collapsePanels() { 278 | ServiceManager.getStatusBarManager().collapsePanels(); 279 | } 280 | 281 | public static String getClipboardText() { 282 | ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); 283 | if (clipboardManager == null) { 284 | return null; 285 | } 286 | CharSequence s = clipboardManager.getText(); 287 | if (s == null) { 288 | return null; 289 | } 290 | return s.toString(); 291 | } 292 | 293 | public boolean setClipboardText(String text) { 294 | ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); 295 | if (clipboardManager == null) { 296 | return false; 297 | } 298 | 299 | String currentClipboard = getClipboardText(); 300 | if (currentClipboard != null && currentClipboard.equals(text)) { 301 | // The clipboard already contains the requested text. 302 | // Since pasting text from the computer involves setting the device clipboard, it could be set twice on a copy-paste. This would cause 303 | // the clipboard listeners to be notified twice, and that would flood the Android keyboard clipboard history. To workaround this 304 | // problem, do not explicitly set the clipboard text if it already contains the expected content. 305 | return false; 306 | } 307 | 308 | isSettingClipboard.set(true); 309 | boolean ok = clipboardManager.setText(text); 310 | isSettingClipboard.set(false); 311 | return ok; 312 | } 313 | 314 | /** 315 | * @param mode one of the {@code POWER_MODE_*} constants 316 | */ 317 | public static boolean setScreenPowerMode(int mode) { 318 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 319 | // On Android 14, these internal methods have been moved to DisplayControl 320 | boolean useDisplayControl = 321 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasPhysicalDisplayIdsMethod(); 322 | 323 | // Change the power mode for all physical displays 324 | long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds(); 325 | if (physicalDisplayIds == null) { 326 | Ln.e("Could not get physical display ids"); 327 | return false; 328 | } 329 | 330 | boolean allOk = true; 331 | for (long physicalDisplayId : physicalDisplayIds) { 332 | IBinder binder = useDisplayControl ? DisplayControl.getPhysicalDisplayToken( 333 | physicalDisplayId) : SurfaceControl.getPhysicalDisplayToken(physicalDisplayId); 334 | allOk &= SurfaceControl.setDisplayPowerMode(binder, mode); 335 | } 336 | return allOk; 337 | } 338 | 339 | // Older Android versions, only 1 display 340 | IBinder d = SurfaceControl.getBuiltInDisplay(); 341 | if (d == null) { 342 | Ln.e("Could not get built-in display"); 343 | return false; 344 | } 345 | return SurfaceControl.setDisplayPowerMode(d, mode); 346 | } 347 | 348 | public static boolean powerOffScreen(int displayId) { 349 | if (!isScreenOn()) { 350 | return true; 351 | } 352 | return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); 353 | } 354 | 355 | /** 356 | * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). 357 | */ 358 | public static void rotateDevice() { 359 | WindowManager wm = ServiceManager.getWindowManager(); 360 | 361 | boolean accelerometerRotation = !wm.isRotationFrozen(); 362 | 363 | int currentRotation = wm.getRotation(); 364 | int newRotation = (currentRotation & 1) ^ 1; // 0->1, 1->0, 2->1, 3->0 365 | String newRotationString = newRotation == 0 ? "portrait" : "landscape"; 366 | 367 | Ln.i("Device rotation requested: " + newRotationString); 368 | wm.freezeRotation(newRotation); 369 | 370 | // restore auto-rotate if necessary 371 | if (accelerometerRotation) { 372 | wm.thawRotation(); 373 | } 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /porting/server/src/main/java/com/genymobile/scrcpy/Controller.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import com.genymobile.scrcpy.wrappers.InputManager; 4 | 5 | import android.os.Build; 6 | import android.os.SystemClock; 7 | import android.view.InputDevice; 8 | import android.view.KeyCharacterMap; 9 | import android.view.KeyEvent; 10 | import android.view.MotionEvent; 11 | 12 | import java.io.IOException; 13 | import java.util.concurrent.Executors; 14 | import java.util.concurrent.ScheduledExecutorService; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | public class Controller implements AsyncProcessor { 18 | 19 | private static final int DEFAULT_DEVICE_ID = 0; 20 | 21 | // control_msg.h values of the pointerId field in inject_touch_event message 22 | private static final int POINTER_ID_MOUSE = -1; 23 | private static final int POINTER_ID_VIRTUAL_MOUSE = -3; 24 | 25 | private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); 26 | 27 | private Thread thread; 28 | 29 | private final Device device; 30 | private final DesktopConnection connection; 31 | private final DeviceMessageSender sender; 32 | private final boolean clipboardAutosync; 33 | private final boolean powerOn; 34 | 35 | private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); 36 | 37 | private long lastTouchDown; 38 | private long lastTouchCont; 39 | private final PointersState pointersState = new PointersState(); 40 | private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; 41 | private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; 42 | 43 | private boolean keepPowerModeOff; 44 | 45 | public Controller(Device device, DesktopConnection connection, boolean clipboardAutosync, boolean powerOn) { 46 | this.device = device; 47 | this.connection = connection; 48 | this.clipboardAutosync = clipboardAutosync; 49 | this.powerOn = powerOn; 50 | initPointers(); 51 | sender = new DeviceMessageSender(connection); 52 | } 53 | 54 | private void initPointers() { 55 | for (int i = 0; i < PointersState.MAX_POINTERS; ++i) { 56 | MotionEvent.PointerProperties props = new MotionEvent.PointerProperties(); 57 | props.toolType = MotionEvent.TOOL_TYPE_FINGER; 58 | 59 | MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); 60 | coords.orientation = 0; 61 | coords.size = 0; 62 | 63 | pointerProperties[i] = props; 64 | pointerCoords[i] = coords; 65 | } 66 | } 67 | 68 | private void control() throws IOException { 69 | // on start, power on the device 70 | if (powerOn && !Device.isScreenOn()) { 71 | device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); 72 | 73 | // dirty hack 74 | // After POWER is injected, the device is powered on asynchronously. 75 | // To turn the device screen off while mirroring, the client will send a message that 76 | // would be handled before the device is actually powered on, so its effect would 77 | // be "canceled" once the device is turned back on. 78 | // Adding this delay prevents to handle the message before the device is actually 79 | // powered on. 80 | SystemClock.sleep(500); 81 | } 82 | 83 | while (!Thread.currentThread().isInterrupted()) { 84 | handleEvent(); 85 | } 86 | } 87 | 88 | @Override 89 | public void start(TerminationListener listener) { 90 | thread = new Thread(() -> { 91 | try { 92 | control(); 93 | } catch (IOException e) { 94 | // this is expected on close 95 | } finally { 96 | Ln.d("Controller stopped"); 97 | listener.onTerminated(true); 98 | } 99 | }, "control-recv"); 100 | thread.start(); 101 | sender.start(); 102 | } 103 | 104 | @Override 105 | public void stop() { 106 | if (thread != null) { 107 | thread.interrupt(); 108 | } 109 | sender.stop(); 110 | } 111 | 112 | @Override 113 | public void join() throws InterruptedException { 114 | if (thread != null) { 115 | thread.join(); 116 | } 117 | sender.join(); 118 | } 119 | 120 | public DeviceMessageSender getSender() { 121 | return sender; 122 | } 123 | 124 | private void handleEvent() throws IOException { 125 | ControlMessage msg = connection.receiveControlMessage(); 126 | switch (msg.getType()) { 127 | case ControlMessage.TYPE_INJECT_KEYCODE: 128 | if (device.supportsInputEvents()) { 129 | injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState()); 130 | } 131 | break; 132 | case ControlMessage.TYPE_INJECT_TEXT: 133 | if (device.supportsInputEvents()) { 134 | injectText(msg.getText()); 135 | } 136 | break; 137 | case ControlMessage.TYPE_INJECT_TOUCH_EVENT: 138 | if (device.supportsInputEvents()) { 139 | injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons()); 140 | } 141 | break; 142 | case ControlMessage.TYPE_INJECT_SCROLL_EVENT: 143 | if (device.supportsInputEvents()) { 144 | injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll(), msg.getButtons()); 145 | } 146 | break; 147 | case ControlMessage.TYPE_BACK_OR_SCREEN_ON: 148 | if (device.supportsInputEvents()) { 149 | pressBackOrTurnScreenOn(msg.getAction()); 150 | } 151 | break; 152 | case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: 153 | Device.expandNotificationPanel(); 154 | break; 155 | case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: 156 | Device.expandSettingsPanel(); 157 | break; 158 | case ControlMessage.TYPE_COLLAPSE_PANELS: 159 | Device.collapsePanels(); 160 | break; 161 | case ControlMessage.TYPE_GET_CLIPBOARD: 162 | getClipboard(msg.getCopyKey()); 163 | break; 164 | case ControlMessage.TYPE_SET_CLIPBOARD: 165 | setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); 166 | break; 167 | case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: 168 | if (device.supportsInputEvents()) { 169 | int mode = msg.getAction(); 170 | boolean setPowerModeOk = Device.setScreenPowerMode(mode); 171 | if (setPowerModeOk) { 172 | keepPowerModeOff = mode == Device.POWER_MODE_OFF; 173 | Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); 174 | } 175 | } 176 | break; 177 | case ControlMessage.TYPE_ROTATE_DEVICE: 178 | Device.rotateDevice(); 179 | break; 180 | default: 181 | // do nothing 182 | } 183 | } 184 | 185 | private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { 186 | // handle keycode 'END' to trigger codec restart 187 | if (keycode == KeyEvent.KEYCODE_MOVE_END) { 188 | Ln.w("Keycode: " + String.format("%d == %d", (int)keycode, KeyEvent.KEYCODE_MOVE_END)); 189 | device.rotationListener.onRotationChanged(0); 190 | return true; 191 | } 192 | 193 | if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { 194 | schedulePowerModeOff(); 195 | } 196 | return device.injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); 197 | } 198 | 199 | private boolean injectChar(char c) { 200 | String decomposed = KeyComposition.decompose(c); 201 | char[] chars = decomposed != null ? decomposed.toCharArray() : new char[]{c}; 202 | KeyEvent[] events = charMap.getEvents(chars); 203 | if (events == null) { 204 | return false; 205 | } 206 | for (KeyEvent event : events) { 207 | if (!device.injectEvent(event, Device.INJECT_MODE_ASYNC)) { 208 | return false; 209 | } 210 | } 211 | return true; 212 | } 213 | 214 | private int injectText(String text) { 215 | int successCount = 0; 216 | for (char c : text.toCharArray()) { 217 | if (!injectChar(c)) { 218 | Ln.w("Could not inject char u+" + String.format("%04x", (int) c)); 219 | continue; 220 | } 221 | successCount++; 222 | } 223 | return successCount; 224 | } 225 | 226 | private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { 227 | long now = SystemClock.uptimeMillis(); 228 | 229 | Point point = device.getPhysicalPoint(position); 230 | if (point == null) { 231 | Ln.w("Ignore touch event, it was generated for a different device size"); 232 | return false; 233 | } 234 | 235 | int pointerIndex = pointersState.getPointerIndex(pointerId); 236 | if (pointerIndex == -1) { 237 | Ln.w("Too many pointers for touch event"); 238 | return false; 239 | } 240 | Pointer pointer = pointersState.get(pointerIndex); 241 | pointer.setPoint(point); 242 | pointer.setPressure(pressure); 243 | 244 | int source; 245 | if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) { 246 | // real mouse event (forced by the client when --forward-on-click) 247 | pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE; 248 | source = InputDevice.SOURCE_MOUSE; 249 | pointer.setUp(buttons == 0); 250 | } else { 251 | // POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device 252 | pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER; 253 | source = InputDevice.SOURCE_TOUCHSCREEN; 254 | // Buttons must not be set for touch events 255 | buttons = 0; 256 | pointer.setUp(action == MotionEvent.ACTION_UP); 257 | } 258 | 259 | int pointerCount = pointersState.update(pointerProperties, pointerCoords); 260 | if (pointerCount == 1) { 261 | if (action == MotionEvent.ACTION_DOWN) { 262 | lastTouchDown = now; 263 | lastTouchCont = now; 264 | } 265 | } else { 266 | // secondary pointers must use ACTION_POINTER_* ORed with the pointerIndex 267 | if (action == MotionEvent.ACTION_UP) { 268 | action = MotionEvent.ACTION_POINTER_UP | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT); 269 | } else if (action == MotionEvent.ACTION_DOWN) { 270 | action = MotionEvent.ACTION_POINTER_DOWN | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT); 271 | } 272 | } 273 | 274 | /* If the input device is a mouse (on API >= 23): 275 | * - the first button pressed must first generate ACTION_DOWN; 276 | * - all button pressed (including the first one) must generate ACTION_BUTTON_PRESS; 277 | * - all button released (including the last one) must generate ACTION_BUTTON_RELEASE; 278 | * - the last button released must in addition generate ACTION_UP. 279 | * 280 | * Otherwise, Chrome does not work properly: 281 | */ 282 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && source == InputDevice.SOURCE_MOUSE) { 283 | if (action == MotionEvent.ACTION_DOWN) { 284 | if (actionButton == buttons) { 285 | // First button pressed: ACTION_DOWN 286 | MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties, 287 | pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); 288 | if (!device.injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) { 289 | return false; 290 | } 291 | } 292 | 293 | // Any button pressed: ACTION_BUTTON_PRESS 294 | MotionEvent pressEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_PRESS, pointerCount, pointerProperties, 295 | pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); 296 | if (!InputManager.setActionButton(pressEvent, actionButton)) { 297 | return false; 298 | } 299 | if (!device.injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) { 300 | return false; 301 | } 302 | 303 | return true; 304 | } 305 | 306 | if (action == MotionEvent.ACTION_UP) { 307 | // Any button released: ACTION_BUTTON_RELEASE 308 | MotionEvent releaseEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_RELEASE, pointerCount, pointerProperties, 309 | pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); 310 | if (!InputManager.setActionButton(releaseEvent, actionButton)) { 311 | return false; 312 | } 313 | if (!device.injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) { 314 | return false; 315 | } 316 | 317 | if (buttons == 0) { 318 | // Last button released: ACTION_UP 319 | MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties, 320 | pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); 321 | if (!device.injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) { 322 | return false; 323 | } 324 | } 325 | 326 | return true; 327 | } 328 | } 329 | 330 | MotionEvent event = MotionEvent 331 | .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 332 | 0); 333 | lastTouchCont += 16; 334 | return device.injectEvent(event, Device.INJECT_MODE_ASYNC); 335 | } 336 | 337 | private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) { 338 | long now = SystemClock.uptimeMillis(); 339 | Point point = device.getPhysicalPoint(position); 340 | if (point == null) { 341 | // ignore event 342 | return false; 343 | } 344 | 345 | MotionEvent.PointerProperties props = pointerProperties[0]; 346 | props.id = 0; 347 | 348 | MotionEvent.PointerCoords coords = pointerCoords[0]; 349 | coords.x = point.getX(); 350 | coords.y = point.getY(); 351 | coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); 352 | coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); 353 | 354 | MotionEvent event = MotionEvent 355 | .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, 356 | InputDevice.SOURCE_MOUSE, 0); 357 | return device.injectEvent(event, Device.INJECT_MODE_ASYNC); 358 | } 359 | 360 | /** 361 | * Schedule a call to set power mode to off after a small delay. 362 | */ 363 | private static void schedulePowerModeOff() { 364 | EXECUTOR.schedule(() -> { 365 | Ln.i("Forcing screen off"); 366 | Device.setScreenPowerMode(Device.POWER_MODE_OFF); 367 | }, 200, TimeUnit.MILLISECONDS); 368 | } 369 | 370 | private boolean pressBackOrTurnScreenOn(int action) { 371 | if (Device.isScreenOn()) { 372 | return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC); 373 | } 374 | 375 | // Screen is off 376 | // Only press POWER on ACTION_DOWN 377 | if (action != KeyEvent.ACTION_DOWN) { 378 | // do nothing, 379 | return true; 380 | } 381 | 382 | if (keepPowerModeOff) { 383 | schedulePowerModeOff(); 384 | } 385 | return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); 386 | } 387 | 388 | private void getClipboard(int copyKey) { 389 | // On Android >= 7, press the COPY or CUT key if requested 390 | if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { 391 | int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT; 392 | // Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one 393 | device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH); 394 | } 395 | 396 | // If clipboard autosync is enabled, then the device clipboard is synchronized to the computer clipboard whenever it changes, in 397 | // particular when COPY or CUT are injected, so it should not be synchronized twice. On Android < 7, do not synchronize at all rather than 398 | // copying an old clipboard content. 399 | if (!clipboardAutosync) { 400 | String clipboardText = Device.getClipboardText(); 401 | if (clipboardText != null) { 402 | sender.pushClipboardText(clipboardText); 403 | } 404 | } 405 | } 406 | 407 | private boolean setClipboard(String text, boolean paste, long sequence) { 408 | boolean ok = device.setClipboardText(text); 409 | if (ok) { 410 | Ln.i("Device clipboard set"); 411 | } 412 | 413 | // On Android >= 7, also press the PASTE key if requested 414 | if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { 415 | device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC); 416 | } 417 | 418 | if (sequence != ControlMessage.SEQUENCE_INVALID) { 419 | // Acknowledgement requested 420 | sender.pushAckClipboard(sequence); 421 | } 422 | 423 | return ok; 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /scrcpy-ios/scrcpy-ios/ScrcpyClient.m: -------------------------------------------------------------------------------- 1 | // 2 | // ScrcpyClient.m 3 | // scrcpy-ios 4 | // 5 | // Created by Ethan on 2022/6/2. 6 | // 7 | 8 | #import "ScrcpyClient.h" 9 | #import "scrcpy-porting.h" 10 | #import "adb_public.h" 11 | #import 12 | #import 13 | 14 | #import 15 | #import 16 | #import 17 | 18 | #import "SDLUIKitDelegate+Extend.h" 19 | 20 | @interface ScrcpyClient () 21 | // Connecting infomations 22 | @property (nonatomic, copy) NSString *connectedSerial; 23 | 24 | // Scrcpy status 25 | @property (nonatomic, assign) enum ScrcpyStatus status; 26 | 27 | // Underlying ADB status change callback 28 | @property (nonatomic, copy) void (^adbStatusUpdated)(NSString *serial, NSString *status); 29 | 30 | // Underlying Scrcpy status change callback 31 | @property (nonatomic, copy) void (^scrcpyStatusUpdated)(enum ScrcpyStatus status); 32 | 33 | // ADB Start Queue 34 | @property (nonatomic, strong) NSOperationQueue *adbStartQueue; 35 | 36 | @end 37 | 38 | CFRunLoopRunResult CFRunLoopRunInMode_fix(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { 39 | // Upper runloop duration to reduce CPU usage 40 | // Audio Thread using seconds == 0.100000001f, so dynamic change 41 | // the runloop duration here can reduce CPU cost 42 | seconds = seconds >= 0.1f ? seconds : ({ 43 | ScrcpySharedClient.enablePowerSavingMode ? 0.002f : 0.0001f; 44 | }); 45 | return CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled); 46 | } 47 | 48 | void adb_connect_status_updated(const char *serial, const char *status) { 49 | NSString *adbSerial = [NSString stringWithUTF8String:serial]; 50 | NSString *adbStatus = [NSString stringWithUTF8String:status]; 51 | if (ScrcpySharedClient.adbStatusUpdated) 52 | ScrcpySharedClient.adbStatusUpdated(adbSerial, adbStatus); 53 | } 54 | 55 | void ScrcpyUpdateStatus(enum ScrcpyStatus status) { 56 | ScrcpySharedClient.status = status; 57 | if (ScrcpySharedClient.scrcpyStatusUpdated) 58 | ScrcpySharedClient.scrcpyStatusUpdated(status); 59 | } 60 | 61 | float screen_scale(void) { 62 | if ([UIScreen.mainScreen respondsToSelector:@selector(nativeScale)]) { 63 | return UIScreen.mainScreen.nativeScale; 64 | } 65 | return UIScreen.mainScreen.scale; 66 | } 67 | 68 | bool ScrcpyEnableHardwareDecoding(void) { 69 | #if TARGET_IPHONE_SIMULATOR 70 | return false; 71 | #else 72 | return true; 73 | #endif 74 | } 75 | 76 | void RenderPixelBufferFrame(CVPixelBufferRef pixelBuffer) { 77 | if (pixelBuffer == NULL) { return; } 78 | 79 | CMSampleTimingInfo timing = {kCMTimeInvalid, kCMTimeInvalid, kCMTimeInvalid}; 80 | CMVideoFormatDescriptionRef videoInfo = NULL; 81 | OSStatus result = CMVideoFormatDescriptionCreateForImageBuffer(NULL, pixelBuffer, &videoInfo); 82 | 83 | CMSampleBufferRef sampleBuffer = NULL; 84 | result = CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault,pixelBuffer, true, NULL, NULL, videoInfo, &timing, &sampleBuffer); 85 | 86 | CFRelease(pixelBuffer); 87 | CFRelease(videoInfo); 88 | 89 | CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES); 90 | CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0); 91 | CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue); 92 | 93 | static AVSampleBufferDisplayLayer *displayLayer = nil; 94 | if (displayLayer == nil || displayLayer.superlayer == nil) { 95 | [displayLayer removeFromSuperlayer]; 96 | displayLayer = [AVSampleBufferDisplayLayer layer]; 97 | displayLayer.videoGravity = AVLayerVideoGravityResizeAspect; 98 | 99 | UIWindow *sdlWindow = [UIApplication.sharedApplication.windows filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(UIWindow *window, id bindings) { 100 | return [NSStringFromClass([window class]) containsString:@"SDL_"]; 101 | }]].firstObject; 102 | 103 | // Skip when no SDL window found 104 | if (sdlWindow == nil) return; 105 | 106 | displayLayer.frame = sdlWindow.rootViewController.view.bounds; 107 | [sdlWindow.rootViewController.view.layer addSublayer:displayLayer]; 108 | sdlWindow.rootViewController.view.backgroundColor = UIColor.blackColor; 109 | // sometimes failed to set background color, so we append to next runloop 110 | displayLayer.backgroundColor = UIColor.blackColor.CGColor; 111 | NSLog(@"[INFO] Using hardware decoding."); 112 | } 113 | 114 | // After become forground from background, may render fail 115 | if (displayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) { 116 | [displayLayer flush]; 117 | } 118 | 119 | // render sampleBuffer now 120 | [displayLayer enqueueSampleBuffer:sampleBuffer]; 121 | } 122 | 123 | void ScrcpyHandleFrame(AVFrame *frame) { 124 | if (ScrcpyEnableHardwareDecoding() == false) { 125 | return; 126 | } 127 | 128 | CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)frame->data[3]; 129 | RenderPixelBufferFrame(pixelBuffer); 130 | } 131 | 132 | @implementation ScrcpyClient 133 | 134 | +(instancetype)sharedClient 135 | { 136 | static ScrcpyClient *client = nil; 137 | static dispatch_once_t onceToken; 138 | dispatch_once(&onceToken, ^{ 139 | client = [[ScrcpyClient alloc] init]; 140 | }); 141 | return client; 142 | } 143 | 144 | +(void)load { 145 | [ScrcpySharedClient setup]; 146 | } 147 | 148 | -(void)setup { 149 | // ADB Settings 150 | self.adbDaemonPort = 15037; 151 | self.adbStartQueue = [[NSOperationQueue alloc] init]; 152 | self.adbStartQueue.maxConcurrentOperationCount = 1; 153 | 154 | // Set ADB Home 155 | NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 156 | self.adbHomePath = documentPaths.firstObject; 157 | 158 | // Add notification to responds background mode changed 159 | [NSNotificationCenter.defaultCenter addObserver:self 160 | selector:@selector(onApplicationEnterForeground) 161 | name:UIApplicationDidBecomeActiveNotification 162 | object:nil]; 163 | 164 | // Add notification to handle URL open 165 | [NSNotificationCenter.defaultCenter addObserver:self 166 | selector:@selector(handleScrcpyURLScheme:) 167 | name:ScrcpyConnectWithSchemeNotification 168 | object:nil]; 169 | } 170 | 171 | #pragma mark - Events 172 | 173 | -(void)onApplicationEnterForeground { 174 | [self checkStartScheme]; 175 | [self onStartOrResume]; 176 | } 177 | 178 | -(void)onStartOrResume { 179 | if (self.status == ScrcpyStatusConnected) { 180 | NSLog(@"-> Send command to trigger video restart I frame"); 181 | [self sendKeycodeEvent:SDL_SCANCODE_END keycode:SDLK_END keymod:0]; 182 | 183 | NSLog(@"-> Syncing clipboard"); 184 | SDL_Event clip_event; 185 | clip_event.type = SDL_CLIPBOARDUPDATE; 186 | 187 | BOOL posted = (SDL_PushEvent(&clip_event) > 0); 188 | NSLog(@"CLIPBOARD EVENT: Post %@", posted? @"Success" : @"Failed"); 189 | } 190 | } 191 | 192 | -(void)handleScrcpyURLScheme:(NSNotification *)notification { 193 | NSURL *openingURL = notification.userInfo[ScrcpyConnectWithSchemeURLKey]; 194 | NSLog(@"-> URL Scheme: %@", openingURL); 195 | if (openingURL == nil || [openingURL.scheme isEqualToString:@"scrcpy2"] == NO) { 196 | NSLog(@"-> Invalid URL Scheme: URL is not supported"); 197 | return; 198 | } 199 | self.pendingScheme = openingURL; 200 | } 201 | 202 | #pragma mark - Scrcpy Lifetime 203 | 204 | -(void)checkStartScheme { 205 | if (self.pendingScheme == nil) { 206 | return; 207 | } 208 | 209 | if ([UIApplication.sharedApplication.windows filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"SELF.isKeyWindow = %@", @YES]].count == 0) { 210 | return; 211 | } 212 | 213 | NSURL *pendingScheme = self.pendingScheme; 214 | // Mark here to avoid both forground notification and viewAppear trigger twice 215 | self.pendingScheme = nil; 216 | 217 | NSString *adbHost = pendingScheme.host; 218 | NSString *adbPort = pendingScheme.port.stringValue ? : @"5555"; 219 | 220 | if (adbHost.length == 0) { 221 | NSLog(@"[ERROR] No adb host found in scheme"); 222 | return; 223 | } 224 | 225 | __block NSArray *scrcpyOptions = self.defaultScrcpyOptions; 226 | NSURLComponents *urlComps = [NSURLComponents componentsWithURL:pendingScheme resolvingAgainstBaseURL:YES]; 227 | [urlComps.queryItems enumerateObjectsUsingBlock:^(NSURLQueryItem *query, NSUInteger idx, BOOL *stop) { 228 | // if value == true, set value to "" in order to match commandline style scrcpy option like --turn-screen-off 229 | NSString *value = [query.value isEqualToString:@"true"] ? @"" : query.value; 230 | scrcpyOptions = [self setScrcpyOption:scrcpyOptions name:query.name value:value]; 231 | 232 | if ([query.name isEqualToString:@"show-nav-buttons"]) { 233 | self.shouldAlwaysShowNavButtons = [query.value boolValue]; 234 | } 235 | 236 | if ([query.name isEqualToString:@"power-saving"]) { 237 | self.enablePowerSavingMode = [query.value boolValue]; 238 | } 239 | }]; 240 | 241 | NSLog(@"-> Scrcpy Options: %@", scrcpyOptions); 242 | [self startWith:adbHost adbPort:adbPort options:scrcpyOptions]; 243 | } 244 | 245 | -(void)startWith:(NSString *)adbHost 246 | adbPort:(NSString *)adbPort 247 | options:(NSArray *)scrcpyOptions 248 | { 249 | if (self.connectedSerial.length != 0 || 250 | self.status == ScrcpyStatusConnected) { 251 | [self stopScrcpy]; 252 | } 253 | 254 | // Connect ADB First 255 | __weak typeof(self) _self = self; 256 | NSMutableDictionary *statusFlags = [NSMutableDictionary dictionary]; 257 | self.adbStatusUpdated = ^(NSString *serial, NSString *status) { 258 | if ([@[@"device", @"unauthorized"] containsObject:status] && 259 | [statusFlags[status] boolValue]) { 260 | NSLog(@"Ignore this status update, because already changed before"); 261 | return; 262 | } 263 | statusFlags[status] = @YES; 264 | [_self onADBStatusChanged:serial status:status options:scrcpyOptions]; 265 | }; 266 | adbPort = adbPort.length == 0 ? @"5555" : adbPort; 267 | 268 | // Connecting callback 269 | if (self.onADBConnecting) { 270 | NSString *serial = [NSString stringWithFormat:@"%@:%@", adbHost, adbPort]; 271 | self.onADBConnecting(serial); 272 | } 273 | 274 | [self.adbStartQueue addOperationWithBlock:^{ 275 | [self adbConnect:adbHost port:adbPort]; 276 | }]; 277 | } 278 | 279 | -(void)onADBStatusChanged:(NSString *)serial 280 | status:(NSString *)status 281 | options:(NSArray *)scrcpyOptions { 282 | NSLog(@"ADB Status Updated: %@ - %@", serial, status); 283 | // Prevent multipile called start 284 | if ([status isEqualToString:@"device"] && 285 | self.status != ScrcpyStatusConnected) { 286 | if (self.onADBConnected != nil) self.onADBConnected(serial); 287 | self.connectedSerial = serial; 288 | [self performSelectorOnMainThread:@selector(startWithOptions:) withObject:scrcpyOptions waitUntilDone:NO]; 289 | } else if ([status isEqualToString:@"unauthorized"]) { 290 | if (self.onADBUnauthorized != nil) self.onADBUnauthorized(serial); 291 | } 292 | } 293 | 294 | -(void)startWithOptions:(NSArray *)scrcpyOptions { 295 | __weak typeof(self) _self = self; 296 | 297 | // Power saving options 298 | if (self.enablePowerSavingMode) { 299 | NSLog(@"Power saving mode enabled, --max-fps set to 30hz"); 300 | scrcpyOptions = [self setScrcpyOption:scrcpyOptions name:@"max-fps" value:@"30"]; 301 | } 302 | 303 | self.scrcpyStatusUpdated = ^(enum ScrcpyStatus status) { 304 | if (status == ScrcpyStatusConnected && _self.onScrcpyConnected) { 305 | _self.onScrcpyConnected(_self.connectedSerial); 306 | [_self onStartOrResume]; 307 | return; 308 | } 309 | 310 | if (status == ScrcpyStatusDisconnected && _self.onScrcpyDisconnected) { 311 | _self.onScrcpyDisconnected(_self.connectedSerial); 312 | return; 313 | } 314 | 315 | if (status == ScrcpyStatusConnectingFailed && _self.onScrcpyConnectFailed) { 316 | _self.onScrcpyConnectFailed(_self.connectedSerial); 317 | return; 318 | } 319 | }; 320 | 321 | CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, NO); 322 | 323 | // Because after SDL proxied didFinishLauch, PumpEvent will set to FASLE 324 | // So we need to set to TRUE in order to handle UI events 325 | SDL_iPhoneSetEventPump(SDL_TRUE); 326 | 327 | // Flush all events include the not proccessed SERVER_DISCONNECT events 328 | SDL_FlushEvents(0, 0xFFFF); 329 | 330 | // Setup arguments 331 | int idx = 0; 332 | const char *args[20]; 333 | args[idx] = "scrcpy"; 334 | while ((++idx) && idx <= scrcpyOptions.count) { 335 | args[idx] = [scrcpyOptions[idx-1] UTF8String]; 336 | } 337 | 338 | ScrcpyUpdateStatus(ScrcpyStatusConnecting); 339 | scrcpy_main((int)scrcpyOptions.count+1, (char **)args); 340 | 341 | ScrcpyUpdateStatus(ScrcpyStatusDisconnected); 342 | } 343 | 344 | -(void)stopScrcpy { 345 | // Call SQL_Quit to send Quit Event 346 | SDL_Event event; 347 | event.type = SDL_QUIT; 348 | SDL_PushEvent(&event); 349 | 350 | // Disconnect ADB core 351 | [self adbDisconnect:nil port:nil]; 352 | 353 | // Wait for scrcpy exited 354 | while (self.status != ScrcpyStatusDisconnected) { 355 | CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, NO); 356 | } 357 | } 358 | 359 | #pragma mark - ADB Lifetime 360 | 361 | -(void)startADBServer { 362 | [self.adbStartQueue addOperationWithBlock:^{ 363 | NSString *message = @""; 364 | BOOL success = [self adbExecute:@[@"start-server"] message:&message]; 365 | NSLog(@"Start ADB Server: %@", success?@"YES":@"NO"); 366 | if (message.length > 0) printf("-> %s\n", message.UTF8String); 367 | }]; 368 | } 369 | 370 | -(void)enableADBVerbose { 371 | adb_enable_trace(); 372 | } 373 | 374 | -(void)setAdbDaemonPort:(NSInteger)adbDaemonPort { 375 | _adbDaemonPort = adbDaemonPort; 376 | adb_set_server_port([NSString stringWithFormat:@"%ld", adbDaemonPort].UTF8String); 377 | } 378 | 379 | -(void)setAdbHomePath:(NSString *)adbHomePath { 380 | _adbHomePath = adbHomePath; 381 | adb_set_home(adbHomePath.UTF8String); 382 | } 383 | 384 | -(void)adbConnect:(NSString *)adbHost port:(NSString *)adbPort { 385 | adbPort = adbPort.length == 0 ? @"5555" : adbPort; 386 | NSString *serial = [NSString stringWithFormat:@"%@:%@", adbHost, adbPort]; 387 | 388 | NSString *devices = nil; 389 | [self adbExecute:@[ @"devices" ] message:&devices]; 390 | NSArray *deviceLines = [devices componentsSeparatedByString:@"\n"]; 391 | for (NSString *device in deviceLines) { 392 | if ([device containsString:serial] && [devices containsString:@"unauthorized"]) { 393 | NSLog(@"adb device unauthorized %@", serial); 394 | if (self.adbStatusUpdated) self.adbStatusUpdated(serial, @"unauthorized"); 395 | return; 396 | } 397 | 398 | if ([device containsString:serial] && [devices containsString:@"device"]) { 399 | NSLog(@"adb device already connected %@", serial); 400 | if (self.adbStatusUpdated) self.adbStatusUpdated(serial, @"device"); 401 | return; 402 | } 403 | } 404 | 405 | // Disconnect all before connect 406 | [self adbDisconnect:nil port:nil]; 407 | 408 | NSString *message = nil; 409 | NSInteger code = [self adbExecute:@[@"connect", serial] message:&message]; 410 | NSLog(@"adb connnect code: %ld, message: %@", code, message); 411 | if ([message containsString:@"failed"] && self.onADBConnectFailed) { 412 | self.onADBConnectFailed(serial, message); 413 | } 414 | 415 | [self adbExecute:@[@"get-serialno"] message:&message]; 416 | NSLog(@"adb get-serialno: %@", message); 417 | } 418 | 419 | -(void)adbDisconnect:(NSString *)adbHost port:(NSString *)adbPort { 420 | NSString *message = nil; 421 | if (adbHost.length == 0) { 422 | [self adbExecute:@[ @"disconnect" ] message:&message]; 423 | if (message.length > 0) { 424 | NSLog(@"adb disconnect: %@", message); 425 | } 426 | return; 427 | } 428 | 429 | adbPort = adbPort.length == 0 ? @"5555" : adbPort; 430 | 431 | NSString *target = [NSString stringWithFormat:@"%@:%@", adbHost, adbPort]; 432 | [self adbExecute:@[ @"disconnect", target ] message:&message]; 433 | if (message.length > 0) { 434 | NSLog(@"adb disconnect: %@", message); 435 | } 436 | } 437 | 438 | -(BOOL)adbExecute:(NSArray *)commands message:(NSString **)message { 439 | NSInteger code = [self adbExecuteUnderlying:commands message:message]; 440 | return code == 0; 441 | } 442 | 443 | -(NSInteger)adbExecuteUnderlying:(NSArray *)commands message:(NSString **)message { 444 | int argc = (int)commands.count; 445 | char *argv[argc]; 446 | for (int i = 0; i < argc; i++) { 447 | argv[i] = strdup(commands[i].UTF8String); 448 | } 449 | char *output_message; 450 | int code = adb_commandline_porting(argc, (const char **)argv, &output_message); 451 | if (message != nil) 452 | *message = output_message == NULL ? nil : [NSString stringWithUTF8String:output_message]; 453 | return (NSInteger)code; 454 | } 455 | 456 | #pragma mark - Scrcpy Options 457 | 458 | -(NSArray *)defaultScrcpyOptions { 459 | return @[ @"--verbosity=debug", @"--shortcut-mod=lctrl+rctrl", 460 | @"--fullscreen", @"--display-buffer=33", 461 | /** @"--video-codec=h265", **/ @"--video-bit-rate=4M", 462 | @"--audio-bit-rate=128K", @"--audio-buffer=60", @"--no-audio", 463 | @"--max-fps=60", @"--print-fps" ]; 464 | } 465 | 466 | -(NSArray *)availableOptions { 467 | return @[ @"max-size", @"video-bit-rate", @"audio-bit-rate", @"audio-buffer", 468 | @"audio-codec", @"no-audio", @"disable-screensaver", @"display-buffer", 469 | @"force-adb-forward", @"max-fps", @"power-off-on-close", @"turn-screen-off", 470 | @"show-touches", @"stay-awake", ]; 471 | } 472 | 473 | -(NSArray *)setScrcpyOption:(NSArray *)options name:(NSString *)name value:(NSString *)value { 474 | // Enable audio 475 | if ([name isEqualToString:@"enable-audio"] && [options containsObject:@"--no-audio"]) { 476 | // Remove --no-audio in default options 477 | NSMutableArray *newOptions = [NSMutableArray arrayWithArray:options]; 478 | [newOptions removeObject:@"--no-audio"]; 479 | return newOptions; 480 | } 481 | 482 | // Check available options 483 | if ([self.availableOptions containsObject:name] == NO) { 484 | NSLog(@"-> Not supported: %@", name); 485 | return options; 486 | } 487 | 488 | // Remove existed defaults options 489 | NSArray *existedOptions = [options filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *option, id bindings) { 490 | return [option containsString:name]; 491 | }]]; 492 | 493 | NSMutableArray *scrcpyOptions = [NSMutableArray arrayWithArray:options]; 494 | [scrcpyOptions removeObjectsInArray:existedOptions]; 495 | 496 | // Add new option 497 | NSString *newOption = [NSString stringWithFormat:@"--%@", name]; 498 | if (value.length > 0) { 499 | newOption = [newOption stringByAppendingFormat:@"=%@", value]; 500 | } 501 | [scrcpyOptions addObject:newOption]; 502 | 503 | return scrcpyOptions; 504 | } 505 | 506 | #pragma mark - Send Keys 507 | 508 | -(void)sendKeycodeEvent:(SDL_Scancode)scancode keycode:(SDL_Keycode)keycode keymod:(SDL_Keymod)keymod { 509 | SDL_Keysym keySym; 510 | keySym.scancode = scancode; 511 | keySym.sym = keycode; 512 | keySym.mod = keymod; 513 | keySym.unused = 1; 514 | 515 | { 516 | SDL_KeyboardEvent keyEvent; 517 | keyEvent.type = SDL_KEYDOWN; 518 | keyEvent.state = SDL_PRESSED; 519 | keyEvent.repeat = '\0'; 520 | keyEvent.keysym = keySym; 521 | 522 | SDL_Event event; 523 | event.type = keyEvent.type; 524 | event.key = keyEvent; 525 | 526 | SDL_PushEvent(&event); 527 | } 528 | 529 | { 530 | SDL_KeyboardEvent keyEvent; 531 | keyEvent.type = SDL_KEYUP; 532 | keyEvent.state = SDL_PRESSED; 533 | keyEvent.repeat = '\0'; 534 | keyEvent.keysym = keySym; 535 | 536 | SDL_Event event; 537 | event.type = keyEvent.type; 538 | event.key = keyEvent; 539 | 540 | SDL_PushEvent(&event); 541 | } 542 | } 543 | 544 | -(void)sendHomeButton { 545 | NSLog(@"-> Send Home Button"); 546 | [self sendKeycodeEvent:SDL_SCANCODE_H keycode:SDLK_h keymod:KMOD_CTRL]; 547 | } 548 | 549 | -(void)sendBackButton { 550 | NSLog(@"-> Send Back Button"); 551 | [self sendKeycodeEvent:SDL_SCANCODE_B keycode:SDLK_b keymod:KMOD_CTRL]; 552 | } 553 | 554 | -(void)sendSwitchAppButton { 555 | NSLog(@"-> Send Switch App Button"); 556 | [self sendKeycodeEvent:SDL_SCANCODE_S keycode:SDLK_s keymod:KMOD_CTRL]; 557 | } 558 | 559 | @end 560 | --------------------------------------------------------------------------------