├── .gitignore ├── Images └── Screenshots │ ├── ipad │ ├── about.png │ ├── camera.png │ ├── cameras.png │ ├── help.png │ ├── scan.png │ ├── settings.png │ └── video.png │ ├── ipad129 │ ├── about.png │ ├── camera.png │ ├── cameras.png │ ├── help.png │ ├── scan.png │ ├── settings.png │ └── video.png │ ├── ipad97 │ ├── about.png │ ├── camera.png │ ├── cameras.png │ ├── help.png │ ├── scan.png │ ├── settings.png │ └── video.png │ ├── iphone4 │ ├── about.png │ ├── camera.png │ ├── cameras.png │ ├── help.png │ ├── scan.png │ ├── settings.png │ └── video.png │ └── iphone55 │ ├── about.png │ ├── camera.png │ ├── cameras.png │ ├── help.png │ ├── scan.png │ ├── settings.png │ └── video.png ├── LICENSE ├── README.md ├── RPiCameraViewer.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings └── RPiCameraViewer ├── AppDelegate.swift ├── Assets.xcassets ├── AppIcon.appiconset │ ├── .DS_Store │ ├── Contents.json │ ├── raspcam_1024.png │ ├── raspcam_120.png │ ├── raspcam_152.png │ ├── raspcam_167.png │ ├── raspcam_180.png │ ├── raspcam_20.png │ ├── raspcam_29.png │ ├── raspcam_40.png │ ├── raspcam_58.png │ ├── raspcam_60.png │ ├── raspcam_76.png │ ├── raspcam_80.png │ └── raspcam_87.png ├── Camera.imageset │ ├── Contents.json │ ├── camera-1.png │ ├── camera-2.png │ └── camera.png ├── CloseButton.imageset │ ├── Contents.json │ ├── close_button-1.png │ ├── close_button-2.png │ └── close_button-3.png ├── Contents.json ├── Logo.imageset │ ├── Contents.json │ ├── logo-1.png │ ├── logo-2.png │ └── logo.png ├── SnapshotButton.imageset │ ├── Contents.json │ ├── snapshot_button-1.png │ ├── snapshot_button-2.png │ └── snapshot_button-3.png ├── TabAbout.imageset │ ├── Contents.json │ ├── Info-25.png │ ├── Info-50.png │ └── Info-75.png ├── TabCamera.imageset │ ├── Camera-25.png │ ├── Camera-50.png │ ├── Camera-75.png │ └── Contents.json ├── TabHelp.imageset │ ├── Contents.json │ ├── Help-25.png │ ├── Help-50.png │ └── Help-75.png ├── TabSettings.imageset │ ├── Contents.json │ ├── Settings-25.png │ ├── Settings-50.png │ └── Settings-75.png └── WhiteXButton.imageset │ ├── Contents.json │ ├── white_x_button-1.png │ ├── white_x_button-2.png │ └── white_x_button-3.png ├── Base.lproj ├── LaunchScreen.storyboard └── Main.storyboard ├── Bridging-Header.h ├── Classes ├── Camera.swift ├── NavigationControllerExt.swift ├── NetworkInterface.swift ├── Reachability.swift ├── Settings.swift ├── StringExt.swift ├── Utils.swift └── ZoomPan.swift ├── Info.plist ├── Localizable.strings ├── RPiCameraViewer.entitlements ├── Sockets ├── mysocket.c └── mysocket.h ├── ViewControllers ├── AboutViewController.swift ├── CameraViewController.swift ├── CamerasViewController.swift ├── HelpViewController.swift ├── InputViewController.swift ├── ScanningViewController.swift ├── SettingsViewController.swift ├── TabBarController.swift └── VideoViewController.swift ├── Views ├── Button.swift ├── IntTextField.swift └── Popup.swift └── en.lproj └── Main.strings /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots 68 | fastlane/test_output 69 | 70 | .DS_Store 71 | -------------------------------------------------------------------------------- /Images/Screenshots/ipad/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad/about.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad/camera.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad/cameras.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad/cameras.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad/help.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad/scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad/scan.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad/settings.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad/video.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad129/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad129/about.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad129/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad129/camera.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad129/cameras.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad129/cameras.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad129/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad129/help.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad129/scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad129/scan.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad129/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad129/settings.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad129/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad129/video.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad97/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad97/about.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad97/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad97/camera.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad97/cameras.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad97/cameras.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad97/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad97/help.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad97/scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad97/scan.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad97/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad97/settings.png -------------------------------------------------------------------------------- /Images/Screenshots/ipad97/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/ipad97/video.png -------------------------------------------------------------------------------- /Images/Screenshots/iphone4/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/iphone4/about.png -------------------------------------------------------------------------------- /Images/Screenshots/iphone4/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/iphone4/camera.png -------------------------------------------------------------------------------- /Images/Screenshots/iphone4/cameras.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/iphone4/cameras.png -------------------------------------------------------------------------------- /Images/Screenshots/iphone4/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/iphone4/help.png -------------------------------------------------------------------------------- /Images/Screenshots/iphone4/scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/iphone4/scan.png -------------------------------------------------------------------------------- /Images/Screenshots/iphone4/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/iphone4/settings.png -------------------------------------------------------------------------------- /Images/Screenshots/iphone4/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/iphone4/video.png -------------------------------------------------------------------------------- /Images/Screenshots/iphone55/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/iphone55/about.png -------------------------------------------------------------------------------- /Images/Screenshots/iphone55/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/iphone55/camera.png -------------------------------------------------------------------------------- /Images/Screenshots/iphone55/cameras.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/iphone55/cameras.png -------------------------------------------------------------------------------- /Images/Screenshots/iphone55/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/iphone55/help.png -------------------------------------------------------------------------------- /Images/Screenshots/iphone55/scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/iphone55/scan.png -------------------------------------------------------------------------------- /Images/Screenshots/iphone55/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/iphone55/settings.png -------------------------------------------------------------------------------- /Images/Screenshots/iphone55/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/Images/Screenshots/iphone55/video.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Shawn Baker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RPi Camera Viewer 2 | 3 | This program plays the raw H.264 video from a Raspberry Pi. 4 | 5 | ## Copyright and License 6 | 7 | Copyright © 2016-2019 Shawn Baker using the [MIT License](https://opensource.org/licenses/MIT). 8 | 9 | Uses the [Reachability.swift](https://github.com/ashleymills/Reachability.swift) class to detect network changes. 10 | 11 | Raspberry image by [Martin Bérubé](http://www.how-to-draw-funny-cartoons.com), 12 | camera image by [Oxygen Team](http://www.oxygen-icons.org), 13 | tab bar icons by [icons8](https://icons8.com). 14 | 15 | ## Instructions 16 | 17 | [RPi Camera Viewer for iOS](http://frozen.ca/rpi-camera-viewer-for-ios/) 18 | 19 | [Streaming Raw H.264 From A Raspberry Pi](http://frozen.ca/streaming-raw-h-264-from-a-raspberry-pi) 20 | -------------------------------------------------------------------------------- /RPiCameraViewer.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 291EB6D421E71536007CFB93 /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291EB6D321E71536007CFB93 /* Reachability.swift */; }; 11 | 2976994A21DC5A3600718766 /* NavigationControllerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2976994921DC5A3600718766 /* NavigationControllerExt.swift */; }; 12 | 29A295FE1FBA5F76004A7F77 /* NetworkInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A295FD1FBA5F76004A7F77 /* NetworkInterface.swift */; }; 13 | 29E792911FB4FBEA0083CA75 /* ZoomPan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29E792901FB4FBEA0083CA75 /* ZoomPan.swift */; }; 14 | 29FAE7A31FB38CBB00747217 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7A21FB38CBB00747217 /* AppDelegate.swift */; }; 15 | 29FAE7AA1FB38CBB00747217 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 29FAE7A91FB38CBB00747217 /* Assets.xcassets */; }; 16 | 29FAE7BA1FB38EA400747217 /* StringExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7B51FB38EA300747217 /* StringExt.swift */; }; 17 | 29FAE7BB1FB38EA400747217 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7B61FB38EA300747217 /* Settings.swift */; }; 18 | 29FAE7BD1FB38EA400747217 /* Camera.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7B81FB38EA300747217 /* Camera.swift */; }; 19 | 29FAE7BE1FB38EA400747217 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7B91FB38EA300747217 /* Utils.swift */; }; 20 | 29FAE7CB1FB38F1800747217 /* VideoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7C21FB38F1600747217 /* VideoViewController.swift */; }; 21 | 29FAE7CC1FB38F1800747217 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7C31FB38F1600747217 /* SettingsViewController.swift */; }; 22 | 29FAE7CD1FB38F1800747217 /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7C41FB38F1700747217 /* TabBarController.swift */; }; 23 | 29FAE7CE1FB38F1800747217 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7C51FB38F1700747217 /* AboutViewController.swift */; }; 24 | 29FAE7CF1FB38F1800747217 /* CameraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7C61FB38F1700747217 /* CameraViewController.swift */; }; 25 | 29FAE7D01FB38F1800747217 /* HelpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7C71FB38F1700747217 /* HelpViewController.swift */; }; 26 | 29FAE7D11FB38F1800747217 /* CamerasViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7C81FB38F1700747217 /* CamerasViewController.swift */; }; 27 | 29FAE7D21FB38F1800747217 /* InputViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7C91FB38F1700747217 /* InputViewController.swift */; }; 28 | 29FAE7D31FB38F1800747217 /* ScanningViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7CA1FB38F1700747217 /* ScanningViewController.swift */; }; 29 | 29FAE7D71FB38F2400747217 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7D41FB38F2300747217 /* Button.swift */; }; 30 | 29FAE7D81FB38F2400747217 /* IntTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7D51FB38F2300747217 /* IntTextField.swift */; }; 31 | 29FAE7D91FB38F2400747217 /* Popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7D61FB38F2400747217 /* Popup.swift */; }; 32 | 29FAE7DC1FB38F3D00747217 /* mysocket.c in Sources */ = {isa = PBXBuildFile; fileRef = 29FAE7DA1FB38F3C00747217 /* mysocket.c */; }; 33 | 29FAE7EF1FB3939400747217 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 29FAE7EE1FB3939300747217 /* Localizable.strings */; }; 34 | 29FAE7F51FB39D2D00747217 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 29FAE7F11FB39D2D00747217 /* LaunchScreen.storyboard */; }; 35 | 29FAE7F61FB39D2D00747217 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 29FAE7F31FB39D2D00747217 /* Main.storyboard */; }; 36 | 29FAE7FA1FB39DAF00747217 /* Main.strings in Resources */ = {isa = PBXBuildFile; fileRef = 29FAE7F81FB39DAE00747217 /* Main.strings */; }; 37 | /* End PBXBuildFile section */ 38 | 39 | /* Begin PBXFileReference section */ 40 | 291EB6D321E71536007CFB93 /* Reachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reachability.swift; sourceTree = ""; }; 41 | 2976994921DC5A3600718766 /* NavigationControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationControllerExt.swift; sourceTree = ""; }; 42 | 297ECB7721DC0C1A00DC8869 /* RPiCameraViewer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RPiCameraViewer.entitlements; sourceTree = ""; }; 43 | 29A295FD1FBA5F76004A7F77 /* NetworkInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkInterface.swift; sourceTree = ""; }; 44 | 29E792901FB4FBEA0083CA75 /* ZoomPan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomPan.swift; sourceTree = ""; }; 45 | 29FAE79F1FB38CBB00747217 /* RPiCameraViewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RPiCameraViewer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 29FAE7A21FB38CBB00747217 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 47 | 29FAE7A91FB38CBB00747217 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 48 | 29FAE7AE1FB38CBB00747217 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | 29FAE7B51FB38EA300747217 /* StringExt.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExt.swift; sourceTree = ""; }; 50 | 29FAE7B61FB38EA300747217 /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; 51 | 29FAE7B81FB38EA300747217 /* Camera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Camera.swift; sourceTree = ""; }; 52 | 29FAE7B91FB38EA300747217 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; 53 | 29FAE7C21FB38F1600747217 /* VideoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoViewController.swift; sourceTree = ""; }; 54 | 29FAE7C31FB38F1600747217 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 55 | 29FAE7C41FB38F1700747217 /* TabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = ""; }; 56 | 29FAE7C51FB38F1700747217 /* AboutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; 57 | 29FAE7C61FB38F1700747217 /* CameraViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraViewController.swift; sourceTree = ""; }; 58 | 29FAE7C71FB38F1700747217 /* HelpViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelpViewController.swift; sourceTree = ""; }; 59 | 29FAE7C81FB38F1700747217 /* CamerasViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CamerasViewController.swift; sourceTree = ""; }; 60 | 29FAE7C91FB38F1700747217 /* InputViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputViewController.swift; sourceTree = ""; }; 61 | 29FAE7CA1FB38F1700747217 /* ScanningViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScanningViewController.swift; sourceTree = ""; }; 62 | 29FAE7D41FB38F2300747217 /* Button.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; 63 | 29FAE7D51FB38F2300747217 /* IntTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntTextField.swift; sourceTree = ""; }; 64 | 29FAE7D61FB38F2400747217 /* Popup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Popup.swift; sourceTree = ""; }; 65 | 29FAE7DA1FB38F3C00747217 /* mysocket.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = mysocket.c; sourceTree = ""; }; 66 | 29FAE7DB1FB38F3D00747217 /* mysocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = mysocket.h; sourceTree = ""; }; 67 | 29FAE7DD1FB38F4D00747217 /* Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Bridging-Header.h"; sourceTree = ""; }; 68 | 29FAE7EE1FB3939300747217 /* Localizable.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; 69 | 29FAE7F21FB39D2D00747217 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = LaunchScreen.storyboard; sourceTree = ""; }; 70 | 29FAE7F41FB39D2D00747217 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Main.storyboard; sourceTree = ""; }; 71 | 29FAE7F91FB39DAE00747217 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Main.strings; sourceTree = ""; }; 72 | /* End PBXFileReference section */ 73 | 74 | /* Begin PBXFrameworksBuildPhase section */ 75 | 29FAE79C1FB38CBB00747217 /* Frameworks */ = { 76 | isa = PBXFrameworksBuildPhase; 77 | buildActionMask = 2147483647; 78 | files = ( 79 | ); 80 | runOnlyForDeploymentPostprocessing = 0; 81 | }; 82 | /* End PBXFrameworksBuildPhase section */ 83 | 84 | /* Begin PBXGroup section */ 85 | 29FAE7961FB38CBB00747217 = { 86 | isa = PBXGroup; 87 | children = ( 88 | 29FAE7A11FB38CBB00747217 /* RPiCameraViewer */, 89 | 29FAE7A01FB38CBB00747217 /* Products */, 90 | ); 91 | sourceTree = ""; 92 | }; 93 | 29FAE7A01FB38CBB00747217 /* Products */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | 29FAE79F1FB38CBB00747217 /* RPiCameraViewer.app */, 97 | ); 98 | name = Products; 99 | sourceTree = ""; 100 | }; 101 | 29FAE7A11FB38CBB00747217 /* RPiCameraViewer */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 29FAE7B41FB38CD100747217 /* Classes */, 105 | 29FAE7BF1FB38ED400747217 /* Sockets */, 106 | 29FAE7C01FB38EDB00747217 /* Views */, 107 | 29FAE7C11FB38EE100747217 /* ViewControllers */, 108 | 29FAE7F01FB39D2D00747217 /* Base.lproj */, 109 | 29FAE7F71FB39DAE00747217 /* en.lproj */, 110 | 29FAE7EE1FB3939300747217 /* Localizable.strings */, 111 | 29FAE7DD1FB38F4D00747217 /* Bridging-Header.h */, 112 | 29FAE7A21FB38CBB00747217 /* AppDelegate.swift */, 113 | 29FAE7A91FB38CBB00747217 /* Assets.xcassets */, 114 | 29FAE7AE1FB38CBB00747217 /* Info.plist */, 115 | 297ECB7721DC0C1A00DC8869 /* RPiCameraViewer.entitlements */, 116 | ); 117 | path = RPiCameraViewer; 118 | sourceTree = ""; 119 | }; 120 | 29FAE7B41FB38CD100747217 /* Classes */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | 29FAE7B81FB38EA300747217 /* Camera.swift */, 124 | 29FAE7B61FB38EA300747217 /* Settings.swift */, 125 | 29FAE7B51FB38EA300747217 /* StringExt.swift */, 126 | 29FAE7B91FB38EA300747217 /* Utils.swift */, 127 | 29A295FD1FBA5F76004A7F77 /* NetworkInterface.swift */, 128 | 29E792901FB4FBEA0083CA75 /* ZoomPan.swift */, 129 | 2976994921DC5A3600718766 /* NavigationControllerExt.swift */, 130 | 291EB6D321E71536007CFB93 /* Reachability.swift */, 131 | ); 132 | path = Classes; 133 | sourceTree = ""; 134 | }; 135 | 29FAE7BF1FB38ED400747217 /* Sockets */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | 29FAE7DA1FB38F3C00747217 /* mysocket.c */, 139 | 29FAE7DB1FB38F3D00747217 /* mysocket.h */, 140 | ); 141 | path = Sockets; 142 | sourceTree = ""; 143 | }; 144 | 29FAE7C01FB38EDB00747217 /* Views */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | 29FAE7D41FB38F2300747217 /* Button.swift */, 148 | 29FAE7D51FB38F2300747217 /* IntTextField.swift */, 149 | 29FAE7D61FB38F2400747217 /* Popup.swift */, 150 | ); 151 | path = Views; 152 | sourceTree = ""; 153 | }; 154 | 29FAE7C11FB38EE100747217 /* ViewControllers */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | 29FAE7C51FB38F1700747217 /* AboutViewController.swift */, 158 | 29FAE7C81FB38F1700747217 /* CamerasViewController.swift */, 159 | 29FAE7C61FB38F1700747217 /* CameraViewController.swift */, 160 | 29FAE7C71FB38F1700747217 /* HelpViewController.swift */, 161 | 29FAE7C91FB38F1700747217 /* InputViewController.swift */, 162 | 29FAE7CA1FB38F1700747217 /* ScanningViewController.swift */, 163 | 29FAE7C31FB38F1600747217 /* SettingsViewController.swift */, 164 | 29FAE7C41FB38F1700747217 /* TabBarController.swift */, 165 | 29FAE7C21FB38F1600747217 /* VideoViewController.swift */, 166 | ); 167 | path = ViewControllers; 168 | sourceTree = ""; 169 | }; 170 | 29FAE7F01FB39D2D00747217 /* Base.lproj */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | 29FAE7F11FB39D2D00747217 /* LaunchScreen.storyboard */, 174 | 29FAE7F31FB39D2D00747217 /* Main.storyboard */, 175 | ); 176 | path = Base.lproj; 177 | sourceTree = ""; 178 | }; 179 | 29FAE7F71FB39DAE00747217 /* en.lproj */ = { 180 | isa = PBXGroup; 181 | children = ( 182 | 29FAE7F81FB39DAE00747217 /* Main.strings */, 183 | ); 184 | path = en.lproj; 185 | sourceTree = ""; 186 | }; 187 | /* End PBXGroup section */ 188 | 189 | /* Begin PBXNativeTarget section */ 190 | 29FAE79E1FB38CBB00747217 /* RPiCameraViewer */ = { 191 | isa = PBXNativeTarget; 192 | buildConfigurationList = 29FAE7B11FB38CBB00747217 /* Build configuration list for PBXNativeTarget "RPiCameraViewer" */; 193 | buildPhases = ( 194 | 29FAE79B1FB38CBB00747217 /* Sources */, 195 | 29FAE79C1FB38CBB00747217 /* Frameworks */, 196 | 29FAE79D1FB38CBB00747217 /* Resources */, 197 | ); 198 | buildRules = ( 199 | ); 200 | dependencies = ( 201 | ); 202 | name = RPiCameraViewer; 203 | productName = RPiCameraViewer; 204 | productReference = 29FAE79F1FB38CBB00747217 /* RPiCameraViewer.app */; 205 | productType = "com.apple.product-type.application"; 206 | }; 207 | /* End PBXNativeTarget section */ 208 | 209 | /* Begin PBXProject section */ 210 | 29FAE7971FB38CBB00747217 /* Project object */ = { 211 | isa = PBXProject; 212 | attributes = { 213 | LastSwiftUpdateCheck = 0910; 214 | LastUpgradeCheck = 0910; 215 | ORGANIZATIONNAME = "Shawn Baker"; 216 | TargetAttributes = { 217 | 29FAE79E1FB38CBB00747217 = { 218 | CreatedOnToolsVersion = 9.1; 219 | LastSwiftMigration = 0910; 220 | ProvisioningStyle = Automatic; 221 | SystemCapabilities = { 222 | com.apple.AccessWiFi = { 223 | enabled = 1; 224 | }; 225 | }; 226 | }; 227 | }; 228 | }; 229 | buildConfigurationList = 29FAE79A1FB38CBB00747217 /* Build configuration list for PBXProject "RPiCameraViewer" */; 230 | compatibilityVersion = "Xcode 8.0"; 231 | developmentRegion = en; 232 | hasScannedForEncodings = 0; 233 | knownRegions = ( 234 | en, 235 | Base, 236 | ); 237 | mainGroup = 29FAE7961FB38CBB00747217; 238 | productRefGroup = 29FAE7A01FB38CBB00747217 /* Products */; 239 | projectDirPath = ""; 240 | projectRoot = ""; 241 | targets = ( 242 | 29FAE79E1FB38CBB00747217 /* RPiCameraViewer */, 243 | ); 244 | }; 245 | /* End PBXProject section */ 246 | 247 | /* Begin PBXResourcesBuildPhase section */ 248 | 29FAE79D1FB38CBB00747217 /* Resources */ = { 249 | isa = PBXResourcesBuildPhase; 250 | buildActionMask = 2147483647; 251 | files = ( 252 | 29FAE7F51FB39D2D00747217 /* LaunchScreen.storyboard in Resources */, 253 | 29FAE7EF1FB3939400747217 /* Localizable.strings in Resources */, 254 | 29FAE7AA1FB38CBB00747217 /* Assets.xcassets in Resources */, 255 | 29FAE7F61FB39D2D00747217 /* Main.storyboard in Resources */, 256 | 29FAE7FA1FB39DAF00747217 /* Main.strings in Resources */, 257 | ); 258 | runOnlyForDeploymentPostprocessing = 0; 259 | }; 260 | /* End PBXResourcesBuildPhase section */ 261 | 262 | /* Begin PBXSourcesBuildPhase section */ 263 | 29FAE79B1FB38CBB00747217 /* Sources */ = { 264 | isa = PBXSourcesBuildPhase; 265 | buildActionMask = 2147483647; 266 | files = ( 267 | 29FAE7BA1FB38EA400747217 /* StringExt.swift in Sources */, 268 | 29FAE7CE1FB38F1800747217 /* AboutViewController.swift in Sources */, 269 | 29FAE7D01FB38F1800747217 /* HelpViewController.swift in Sources */, 270 | 29FAE7CB1FB38F1800747217 /* VideoViewController.swift in Sources */, 271 | 29FAE7CD1FB38F1800747217 /* TabBarController.swift in Sources */, 272 | 29FAE7D21FB38F1800747217 /* InputViewController.swift in Sources */, 273 | 29FAE7D31FB38F1800747217 /* ScanningViewController.swift in Sources */, 274 | 29FAE7CF1FB38F1800747217 /* CameraViewController.swift in Sources */, 275 | 29FAE7D81FB38F2400747217 /* IntTextField.swift in Sources */, 276 | 2976994A21DC5A3600718766 /* NavigationControllerExt.swift in Sources */, 277 | 29FAE7BD1FB38EA400747217 /* Camera.swift in Sources */, 278 | 29A295FE1FBA5F76004A7F77 /* NetworkInterface.swift in Sources */, 279 | 29FAE7D91FB38F2400747217 /* Popup.swift in Sources */, 280 | 29E792911FB4FBEA0083CA75 /* ZoomPan.swift in Sources */, 281 | 29FAE7BE1FB38EA400747217 /* Utils.swift in Sources */, 282 | 29FAE7BB1FB38EA400747217 /* Settings.swift in Sources */, 283 | 29FAE7A31FB38CBB00747217 /* AppDelegate.swift in Sources */, 284 | 29FAE7CC1FB38F1800747217 /* SettingsViewController.swift in Sources */, 285 | 291EB6D421E71536007CFB93 /* Reachability.swift in Sources */, 286 | 29FAE7D11FB38F1800747217 /* CamerasViewController.swift in Sources */, 287 | 29FAE7DC1FB38F3D00747217 /* mysocket.c in Sources */, 288 | 29FAE7D71FB38F2400747217 /* Button.swift in Sources */, 289 | ); 290 | runOnlyForDeploymentPostprocessing = 0; 291 | }; 292 | /* End PBXSourcesBuildPhase section */ 293 | 294 | /* Begin PBXVariantGroup section */ 295 | 29FAE7F11FB39D2D00747217 /* LaunchScreen.storyboard */ = { 296 | isa = PBXVariantGroup; 297 | children = ( 298 | 29FAE7F21FB39D2D00747217 /* Base */, 299 | ); 300 | name = LaunchScreen.storyboard; 301 | sourceTree = ""; 302 | }; 303 | 29FAE7F31FB39D2D00747217 /* Main.storyboard */ = { 304 | isa = PBXVariantGroup; 305 | children = ( 306 | 29FAE7F41FB39D2D00747217 /* Base */, 307 | ); 308 | name = Main.storyboard; 309 | sourceTree = ""; 310 | }; 311 | 29FAE7F81FB39DAE00747217 /* Main.strings */ = { 312 | isa = PBXVariantGroup; 313 | children = ( 314 | 29FAE7F91FB39DAE00747217 /* en */, 315 | ); 316 | name = Main.strings; 317 | sourceTree = ""; 318 | }; 319 | /* End PBXVariantGroup section */ 320 | 321 | /* Begin XCBuildConfiguration section */ 322 | 29FAE7AF1FB38CBB00747217 /* Debug */ = { 323 | isa = XCBuildConfiguration; 324 | buildSettings = { 325 | ALWAYS_SEARCH_USER_PATHS = NO; 326 | CLANG_ANALYZER_NONNULL = YES; 327 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 328 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 329 | CLANG_CXX_LIBRARY = "libc++"; 330 | CLANG_ENABLE_MODULES = YES; 331 | CLANG_ENABLE_OBJC_ARC = YES; 332 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 333 | CLANG_WARN_BOOL_CONVERSION = YES; 334 | CLANG_WARN_COMMA = YES; 335 | CLANG_WARN_CONSTANT_CONVERSION = YES; 336 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 337 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 338 | CLANG_WARN_EMPTY_BODY = YES; 339 | CLANG_WARN_ENUM_CONVERSION = YES; 340 | CLANG_WARN_INFINITE_RECURSION = YES; 341 | CLANG_WARN_INT_CONVERSION = YES; 342 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 343 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 344 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 345 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 346 | CLANG_WARN_STRICT_PROTOTYPES = YES; 347 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 348 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 349 | CLANG_WARN_UNREACHABLE_CODE = YES; 350 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 351 | CODE_SIGN_IDENTITY = "iPhone Developer"; 352 | COPY_PHASE_STRIP = NO; 353 | DEBUG_INFORMATION_FORMAT = dwarf; 354 | ENABLE_STRICT_OBJC_MSGSEND = YES; 355 | ENABLE_TESTABILITY = YES; 356 | GCC_C_LANGUAGE_STANDARD = gnu11; 357 | GCC_DYNAMIC_NO_PIC = NO; 358 | GCC_NO_COMMON_BLOCKS = YES; 359 | GCC_OPTIMIZATION_LEVEL = 0; 360 | GCC_PREPROCESSOR_DEFINITIONS = ( 361 | "DEBUG=1", 362 | "$(inherited)", 363 | ); 364 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 365 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 366 | GCC_WARN_UNDECLARED_SELECTOR = YES; 367 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 368 | GCC_WARN_UNUSED_FUNCTION = YES; 369 | GCC_WARN_UNUSED_VARIABLE = YES; 370 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 371 | MTL_ENABLE_DEBUG_INFO = YES; 372 | ONLY_ACTIVE_ARCH = YES; 373 | SDKROOT = iphoneos; 374 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 375 | SWIFT_OBJC_BRIDGING_HEADER = "RPi Camera Viewer/Bridging-Header.h"; 376 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 377 | }; 378 | name = Debug; 379 | }; 380 | 29FAE7B01FB38CBB00747217 /* Release */ = { 381 | isa = XCBuildConfiguration; 382 | buildSettings = { 383 | ALWAYS_SEARCH_USER_PATHS = NO; 384 | CLANG_ANALYZER_NONNULL = YES; 385 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 386 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 387 | CLANG_CXX_LIBRARY = "libc++"; 388 | CLANG_ENABLE_MODULES = YES; 389 | CLANG_ENABLE_OBJC_ARC = YES; 390 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 391 | CLANG_WARN_BOOL_CONVERSION = YES; 392 | CLANG_WARN_COMMA = YES; 393 | CLANG_WARN_CONSTANT_CONVERSION = YES; 394 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 395 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 396 | CLANG_WARN_EMPTY_BODY = YES; 397 | CLANG_WARN_ENUM_CONVERSION = YES; 398 | CLANG_WARN_INFINITE_RECURSION = YES; 399 | CLANG_WARN_INT_CONVERSION = YES; 400 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 401 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 402 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 403 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 404 | CLANG_WARN_STRICT_PROTOTYPES = YES; 405 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 406 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 407 | CLANG_WARN_UNREACHABLE_CODE = YES; 408 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 409 | CODE_SIGN_IDENTITY = "iPhone Developer"; 410 | COPY_PHASE_STRIP = NO; 411 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 412 | ENABLE_NS_ASSERTIONS = NO; 413 | ENABLE_STRICT_OBJC_MSGSEND = YES; 414 | GCC_C_LANGUAGE_STANDARD = gnu11; 415 | GCC_NO_COMMON_BLOCKS = YES; 416 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 417 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 418 | GCC_WARN_UNDECLARED_SELECTOR = YES; 419 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 420 | GCC_WARN_UNUSED_FUNCTION = YES; 421 | GCC_WARN_UNUSED_VARIABLE = YES; 422 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 423 | MTL_ENABLE_DEBUG_INFO = NO; 424 | SDKROOT = iphoneos; 425 | SWIFT_OBJC_BRIDGING_HEADER = "RPi Camera Viewer/Bridging-Header.h"; 426 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 427 | VALIDATE_PRODUCT = YES; 428 | }; 429 | name = Release; 430 | }; 431 | 29FAE7B21FB38CBB00747217 /* Debug */ = { 432 | isa = XCBuildConfiguration; 433 | buildSettings = { 434 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 435 | CLANG_ENABLE_MODULES = YES; 436 | CODE_SIGN_ENTITLEMENTS = RPiCameraViewer/RPiCameraViewer.entitlements; 437 | CODE_SIGN_STYLE = Automatic; 438 | DEVELOPMENT_TEAM = 3985AHAG8A; 439 | INFOPLIST_FILE = RPiCameraViewer/Info.plist; 440 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 441 | PRODUCT_BUNDLE_IDENTIFIER = ca.frozen.RPiCameraViewer; 442 | PRODUCT_NAME = "$(TARGET_NAME)"; 443 | SWIFT_OBJC_BRIDGING_HEADER = "RPiCameraViewer/Bridging-Header.h"; 444 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 445 | SWIFT_VERSION = 4.0; 446 | TARGETED_DEVICE_FAMILY = "1,2"; 447 | }; 448 | name = Debug; 449 | }; 450 | 29FAE7B31FB38CBB00747217 /* Release */ = { 451 | isa = XCBuildConfiguration; 452 | buildSettings = { 453 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 454 | CLANG_ENABLE_MODULES = YES; 455 | CODE_SIGN_ENTITLEMENTS = RPiCameraViewer/RPiCameraViewer.entitlements; 456 | CODE_SIGN_STYLE = Automatic; 457 | DEVELOPMENT_TEAM = 3985AHAG8A; 458 | INFOPLIST_FILE = RPiCameraViewer/Info.plist; 459 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 460 | PRODUCT_BUNDLE_IDENTIFIER = ca.frozen.RPiCameraViewer; 461 | PRODUCT_NAME = "$(TARGET_NAME)"; 462 | SWIFT_OBJC_BRIDGING_HEADER = "RPiCameraViewer/Bridging-Header.h"; 463 | SWIFT_VERSION = 4.0; 464 | TARGETED_DEVICE_FAMILY = "1,2"; 465 | }; 466 | name = Release; 467 | }; 468 | /* End XCBuildConfiguration section */ 469 | 470 | /* Begin XCConfigurationList section */ 471 | 29FAE79A1FB38CBB00747217 /* Build configuration list for PBXProject "RPiCameraViewer" */ = { 472 | isa = XCConfigurationList; 473 | buildConfigurations = ( 474 | 29FAE7AF1FB38CBB00747217 /* Debug */, 475 | 29FAE7B01FB38CBB00747217 /* Release */, 476 | ); 477 | defaultConfigurationIsVisible = 0; 478 | defaultConfigurationName = Release; 479 | }; 480 | 29FAE7B11FB38CBB00747217 /* Build configuration list for PBXNativeTarget "RPiCameraViewer" */ = { 481 | isa = XCConfigurationList; 482 | buildConfigurations = ( 483 | 29FAE7B21FB38CBB00747217 /* Debug */, 484 | 29FAE7B31FB38CBB00747217 /* Release */, 485 | ); 486 | defaultConfigurationIsVisible = 0; 487 | defaultConfigurationName = Release; 488 | }; 489 | /* End XCConfigurationList section */ 490 | }; 491 | rootObject = 29FAE7971FB38CBB00747217 /* Project object */; 492 | } 493 | -------------------------------------------------------------------------------- /RPiCameraViewer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /RPiCameraViewer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RPiCameraViewer.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /RPiCameraViewer/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2016-2018 Shawn Baker using the MIT License. 2 | import UIKit 3 | 4 | @UIApplicationMain 5 | class AppDelegate: UIResponder, UIApplicationDelegate 6 | { 7 | // instance variables 8 | var window: UIWindow? 9 | var settings = Settings() 10 | var cameras = [Camera]() 11 | var videoViewController: VideoViewController? 12 | 13 | //********************************************************************** 14 | // application 15 | //********************************************************************** 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool 17 | { 18 | // load the settings and cameras 19 | load() 20 | 21 | // set the UI element colors 22 | let barColor = Utils.primaryColor 23 | UINavigationBar.appearance().barTintColor = barColor 24 | UINavigationBar.appearance().tintColor = UIColor.white 25 | UINavigationBar.appearance().titleTextAttributes = [NSAttributedStringKey.foregroundColor:UIColor.white] 26 | UITabBar.appearance().barTintColor = barColor 27 | UITabBar.appearance().tintColor = UIColor.white 28 | UITabBarItem.appearance().setTitleTextAttributes([NSAttributedStringKey.foregroundColor: UIColor.black], for: UIControlState.normal) 29 | UITabBarItem.appearance().setTitleTextAttributes([NSAttributedStringKey.foregroundColor: UIColor.white], for: UIControlState.selected) 30 | UITableViewCell.appearance().tintColor = barColor 31 | 32 | return true 33 | } 34 | 35 | //********************************************************************** 36 | // applicationDidEnterBackground 37 | //********************************************************************** 38 | func applicationDidEnterBackground(_ application: UIApplication) 39 | { 40 | save() 41 | videoViewController?.stop() 42 | } 43 | 44 | //********************************************************************** 45 | // applicationWillEnterForeground 46 | //********************************************************************** 47 | func applicationWillEnterForeground(_ application: UIApplication) 48 | { 49 | videoViewController?.start() 50 | } 51 | 52 | //********************************************************************** 53 | // applicationWillTerminate 54 | //********************************************************************** 55 | func applicationWillTerminate(_ application: UIApplication) 56 | { 57 | save() 58 | } 59 | 60 | //********************************************************************** 61 | // load 62 | //********************************************************************** 63 | func load() 64 | { 65 | // get the settings 66 | let userDefults = UserDefaults.standard 67 | if let data = userDefults.object(forKey: "settings2") as? NSData 68 | { 69 | settings = NSKeyedUnarchiver.unarchiveObject(with: data as Data) as! Settings 70 | } 71 | 72 | // or create the default settings 73 | else 74 | { 75 | let data = NSKeyedArchiver.archivedData(withRootObject: settings); 76 | userDefults.set(data, forKey: "settings2") 77 | } 78 | 79 | // get the list of cameras 80 | if let data = userDefults.object(forKey: "cameras2") as? NSData 81 | { 82 | cameras = NSKeyedUnarchiver.unarchiveObject(with: data as Data) as! [Camera] 83 | } 84 | } 85 | 86 | //********************************************************************** 87 | // save 88 | //********************************************************************** 89 | func save() 90 | { 91 | // save the settings 92 | let userDefults = UserDefaults.standard 93 | var data = NSKeyedArchiver.archivedData(withRootObject: settings); 94 | userDefults.set(data, forKey: "settings2") 95 | 96 | // save the list of cameras 97 | data = NSKeyedArchiver.archivedData(withRootObject: cameras); 98 | userDefults.set(data, forKey: "cameras2") 99 | } 100 | } 101 | 102 | -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/.DS_Store -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "raspcam_40.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "raspcam_60.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "raspcam_58.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "raspcam_87.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "raspcam_80.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "raspcam_120.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "raspcam_120.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "raspcam_180.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "raspcam_20.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "raspcam_40.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "raspcam_29.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "raspcam_58.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "raspcam_40.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "raspcam_80.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "raspcam_76.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "raspcam_152.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "raspcam_167.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "raspcam_1024.png", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_1024.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_120.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_152.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_167.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_180.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_20.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_29.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_40.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_58.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_60.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_76.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_80.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/AppIcon.appiconset/raspcam_87.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/Camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "camera.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "camera-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "camera-2.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/Camera.imageset/camera-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/Camera.imageset/camera-1.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/Camera.imageset/camera-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/Camera.imageset/camera-2.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/Camera.imageset/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/Camera.imageset/camera.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/CloseButton.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "close_button-1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "close_button-2.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "close_button-3.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/CloseButton.imageset/close_button-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/CloseButton.imageset/close_button-1.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/CloseButton.imageset/close_button-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/CloseButton.imageset/close_button-2.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/CloseButton.imageset/close_button-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/CloseButton.imageset/close_button-3.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/Logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "logo.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "logo-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "logo-2.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/Logo.imageset/logo-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/Logo.imageset/logo-1.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/Logo.imageset/logo-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/Logo.imageset/logo-2.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/Logo.imageset/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/Logo.imageset/logo.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/SnapshotButton.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "snapshot_button-1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "snapshot_button-2.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "snapshot_button-3.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/SnapshotButton.imageset/snapshot_button-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/SnapshotButton.imageset/snapshot_button-1.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/SnapshotButton.imageset/snapshot_button-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/SnapshotButton.imageset/snapshot_button-2.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/SnapshotButton.imageset/snapshot_button-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/SnapshotButton.imageset/snapshot_button-3.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/TabAbout.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Info-25.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Info-50.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Info-75.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/TabAbout.imageset/Info-25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/TabAbout.imageset/Info-25.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/TabAbout.imageset/Info-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/TabAbout.imageset/Info-50.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/TabAbout.imageset/Info-75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/TabAbout.imageset/Info-75.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/TabCamera.imageset/Camera-25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/TabCamera.imageset/Camera-25.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/TabCamera.imageset/Camera-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/TabCamera.imageset/Camera-50.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/TabCamera.imageset/Camera-75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/TabCamera.imageset/Camera-75.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/TabCamera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Camera-25.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Camera-50.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Camera-75.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/TabHelp.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Help-25.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Help-50.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Help-75.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/TabHelp.imageset/Help-25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/TabHelp.imageset/Help-25.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/TabHelp.imageset/Help-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/TabHelp.imageset/Help-50.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/TabHelp.imageset/Help-75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/TabHelp.imageset/Help-75.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/TabSettings.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Settings-25.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Settings-50.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Settings-75.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/TabSettings.imageset/Settings-25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/TabSettings.imageset/Settings-25.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/TabSettings.imageset/Settings-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/TabSettings.imageset/Settings-50.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/TabSettings.imageset/Settings-75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/TabSettings.imageset/Settings-75.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/WhiteXButton.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "white_x_button-1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "white_x_button-2.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "white_x_button-3.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/WhiteXButton.imageset/white_x_button-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/WhiteXButton.imageset/white_x_button-1.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/WhiteXButton.imageset/white_x_button-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/WhiteXButton.imageset/white_x_button-2.png -------------------------------------------------------------------------------- /RPiCameraViewer/Assets.xcassets/WhiteXButton.imageset/white_x_button-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnBaker/iOS_RPiCameraViewer/38b4f846b602225ec95eaae6668c10b9bcd36525/RPiCameraViewer/Assets.xcassets/WhiteXButton.imageset/white_x_button-3.png -------------------------------------------------------------------------------- /RPiCameraViewer/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 | -------------------------------------------------------------------------------- /RPiCameraViewer/Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Shawn Baker using the MIT License. 2 | #ifndef Bridging_Header_h 3 | #define Bridging_Header_h 4 | 5 | #include 6 | #import "mysocket.h" 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /RPiCameraViewer/Classes/Camera.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2016-2018 Shawn Baker using the MIT License. 2 | import Foundation 3 | 4 | class Camera: NSObject, NSCoding 5 | { 6 | // instance variables 7 | var name = "" 8 | var network = "" 9 | var address = "" 10 | var port = 5001 11 | 12 | //********************************************************************** 13 | // init 14 | //********************************************************************** 15 | override init() 16 | { 17 | } 18 | 19 | //********************************************************************** 20 | // init 21 | //********************************************************************** 22 | init(_ name: String, _ network: String, _ address: String, _ port: Int) 23 | { 24 | self.name = name 25 | self.network = network 26 | self.address = address 27 | self.port = port 28 | } 29 | 30 | //********************************************************************** 31 | // init 32 | //********************************************************************** 33 | convenience init(_ camera: Camera) 34 | { 35 | self.init(camera.name, camera.network, camera.address, camera.port) 36 | } 37 | 38 | //********************************************************************** 39 | // init 40 | //********************************************************************** 41 | required init(coder decoder: NSCoder) 42 | { 43 | name = decoder.decodeObject(forKey: "name") as! String 44 | network = decoder.decodeObject(forKey: "network") as! String 45 | address = decoder.decodeObject(forKey: "address") as! String 46 | port = decoder.decodeInteger(forKey: "port") 47 | } 48 | 49 | //********************************************************************** 50 | // encode 51 | //********************************************************************** 52 | func encode(with encoder: NSCoder) 53 | { 54 | encoder.encode(name, forKey: "name") 55 | encoder.encode(network, forKey: "network") 56 | encoder.encode(address, forKey: "address") 57 | encoder.encode(port, forKey: "port") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /RPiCameraViewer/Classes/NavigationControllerExt.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Shawn Baker using the MIT License. 2 | import UIKit 3 | 4 | extension UINavigationController 5 | { 6 | //********************************************************************** 7 | // preferredStatusBarStyle 8 | //********************************************************************** 9 | open override var preferredStatusBarStyle: UIStatusBarStyle 10 | { 11 | return .lightContent 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /RPiCameraViewer/Classes/NetworkInterface.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Shawn Baker using the MIT License. 2 | import Foundation 3 | 4 | class NetworkInterface 5 | { 6 | var name: String = "" 7 | var flags: UInt32 = 0 8 | var family: UInt8 = 0 9 | var address: String = "" 10 | var netmask: String = "" 11 | 12 | init() 13 | { 14 | } 15 | 16 | init(_ name: String, _ flags: UInt32, _ family: UInt8, _ address: String, _ netmask: String) 17 | { 18 | self.name = name 19 | self.flags = flags 20 | self.family = family 21 | self.address = address 22 | self.netmask = netmask 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /RPiCameraViewer/Classes/Reachability.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2014, Ashley Mills 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 19 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 25 | POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | import SystemConfiguration 29 | import Foundation 30 | 31 | public enum ReachabilityError: Error { 32 | case FailedToCreateWithAddress(sockaddr_in) 33 | case FailedToCreateWithHostname(String) 34 | case UnableToSetCallback 35 | case UnableToSetDispatchQueue 36 | case UnableToGetInitialFlags 37 | } 38 | 39 | @available(*, unavailable, renamed: "Notification.Name.reachabilityChanged") 40 | public let ReachabilityChangedNotification = NSNotification.Name("ReachabilityChangedNotification") 41 | 42 | public extension Notification.Name { 43 | public static let reachabilityChanged = Notification.Name("reachabilityChanged") 44 | } 45 | 46 | public class Reachability { 47 | 48 | public typealias NetworkReachable = (Reachability) -> () 49 | public typealias NetworkUnreachable = (Reachability) -> () 50 | 51 | @available(*, unavailable, renamed: "Connection") 52 | public enum NetworkStatus: CustomStringConvertible { 53 | case notReachable, reachableViaWiFi, reachableViaWWAN 54 | public var description: String { 55 | switch self { 56 | case .reachableViaWWAN: return "Cellular" 57 | case .reachableViaWiFi: return "WiFi" 58 | case .notReachable: return "No Connection" 59 | } 60 | } 61 | } 62 | 63 | public enum Connection: CustomStringConvertible { 64 | case none, wifi, cellular 65 | public var description: String { 66 | switch self { 67 | case .cellular: return "Cellular" 68 | case .wifi: return "WiFi" 69 | case .none: return "No Connection" 70 | } 71 | } 72 | } 73 | 74 | public var whenReachable: NetworkReachable? 75 | public var whenUnreachable: NetworkUnreachable? 76 | 77 | @available(*, deprecated: 4.0, renamed: "allowsCellularConnection") 78 | public let reachableOnWWAN: Bool = true 79 | 80 | /// Set to `false` to force Reachability.connection to .none when on cellular connection (default value `true`) 81 | public var allowsCellularConnection: Bool 82 | 83 | // The notification center on which "reachability changed" events are being posted 84 | public var notificationCenter: NotificationCenter = NotificationCenter.default 85 | 86 | @available(*, deprecated: 4.0, renamed: "connection.description") 87 | public var currentReachabilityString: String { 88 | return "\(connection)" 89 | } 90 | 91 | @available(*, unavailable, renamed: "connection") 92 | public var currentReachabilityStatus: Connection { 93 | return connection 94 | } 95 | 96 | public var connection: Connection { 97 | if flags == nil { 98 | try? setReachabilityFlags() 99 | } 100 | 101 | switch flags?.connection { 102 | case .none?, nil: return .none 103 | case .cellular?: return allowsCellularConnection ? .cellular : .none 104 | case .wifi?: return .wifi 105 | } 106 | } 107 | 108 | fileprivate var isRunningOnDevice: Bool = { 109 | #if targetEnvironment(simulator) 110 | return false 111 | #else 112 | return true 113 | #endif 114 | }() 115 | 116 | fileprivate var notifierRunning = false 117 | fileprivate let reachabilityRef: SCNetworkReachability 118 | fileprivate let reachabilitySerialQueue: DispatchQueue 119 | fileprivate(set) var flags: SCNetworkReachabilityFlags? { 120 | didSet { 121 | guard flags != oldValue else { return } 122 | reachabilityChanged() 123 | } 124 | } 125 | 126 | required public init(reachabilityRef: SCNetworkReachability, queueQoS: DispatchQoS = .default, targetQueue: DispatchQueue? = nil) { 127 | self.allowsCellularConnection = true 128 | self.reachabilityRef = reachabilityRef 129 | self.reachabilitySerialQueue = DispatchQueue(label: "uk.co.ashleymills.reachability", qos: queueQoS, target: targetQueue) 130 | } 131 | 132 | public convenience init?(hostname: String, queueQoS: DispatchQoS = .default, targetQueue: DispatchQueue? = nil) { 133 | guard let ref = SCNetworkReachabilityCreateWithName(nil, hostname) else { return nil } 134 | self.init(reachabilityRef: ref, queueQoS: queueQoS, targetQueue: targetQueue) 135 | } 136 | 137 | public convenience init?(queueQoS: DispatchQoS = .default, targetQueue: DispatchQueue? = nil) { 138 | var zeroAddress = sockaddr() 139 | zeroAddress.sa_len = UInt8(MemoryLayout.size) 140 | zeroAddress.sa_family = sa_family_t(AF_INET) 141 | 142 | guard let ref = SCNetworkReachabilityCreateWithAddress(nil, &zeroAddress) else { return nil } 143 | 144 | self.init(reachabilityRef: ref, queueQoS: queueQoS, targetQueue: targetQueue) 145 | } 146 | 147 | deinit { 148 | stopNotifier() 149 | } 150 | } 151 | 152 | public extension Reachability { 153 | 154 | // MARK: - *** Notifier methods *** 155 | func startNotifier() throws { 156 | guard !notifierRunning else { return } 157 | 158 | let callback: SCNetworkReachabilityCallBack = { (reachability, flags, info) in 159 | guard let info = info else { return } 160 | 161 | let reachability = Unmanaged.fromOpaque(info).takeUnretainedValue() 162 | reachability.flags = flags 163 | } 164 | 165 | var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil) 166 | context.info = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) 167 | if !SCNetworkReachabilitySetCallback(reachabilityRef, callback, &context) { 168 | stopNotifier() 169 | throw ReachabilityError.UnableToSetCallback 170 | } 171 | 172 | if !SCNetworkReachabilitySetDispatchQueue(reachabilityRef, reachabilitySerialQueue) { 173 | stopNotifier() 174 | throw ReachabilityError.UnableToSetDispatchQueue 175 | } 176 | 177 | // Perform an initial check 178 | try setReachabilityFlags() 179 | 180 | notifierRunning = true 181 | } 182 | 183 | func stopNotifier() { 184 | defer { notifierRunning = false } 185 | 186 | SCNetworkReachabilitySetCallback(reachabilityRef, nil, nil) 187 | SCNetworkReachabilitySetDispatchQueue(reachabilityRef, nil) 188 | } 189 | 190 | // MARK: - *** Connection test methods *** 191 | @available(*, deprecated: 4.0, message: "Please use `connection != .none`") 192 | var isReachable: Bool { 193 | return connection != .none 194 | } 195 | 196 | @available(*, deprecated: 4.0, message: "Please use `connection == .cellular`") 197 | var isReachableViaWWAN: Bool { 198 | // Check we're not on the simulator, we're REACHABLE and check we're on WWAN 199 | return connection == .cellular 200 | } 201 | 202 | @available(*, deprecated: 4.0, message: "Please use `connection == .wifi`") 203 | var isReachableViaWiFi: Bool { 204 | return connection == .wifi 205 | } 206 | 207 | var description: String { 208 | guard let flags = flags else { return "unavailable flags" } 209 | let W = isRunningOnDevice ? (flags.isOnWWANFlagSet ? "W" : "-") : "X" 210 | let R = flags.isReachableFlagSet ? "R" : "-" 211 | let c = flags.isConnectionRequiredFlagSet ? "c" : "-" 212 | let t = flags.isTransientConnectionFlagSet ? "t" : "-" 213 | let i = flags.isInterventionRequiredFlagSet ? "i" : "-" 214 | let C = flags.isConnectionOnTrafficFlagSet ? "C" : "-" 215 | let D = flags.isConnectionOnDemandFlagSet ? "D" : "-" 216 | let l = flags.isLocalAddressFlagSet ? "l" : "-" 217 | let d = flags.isDirectFlagSet ? "d" : "-" 218 | 219 | return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)" 220 | } 221 | } 222 | 223 | fileprivate extension Reachability { 224 | 225 | func setReachabilityFlags() throws { 226 | try reachabilitySerialQueue.sync { [unowned self] in 227 | var flags = SCNetworkReachabilityFlags() 228 | if !SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags) { 229 | self.stopNotifier() 230 | throw ReachabilityError.UnableToGetInitialFlags 231 | } 232 | 233 | self.flags = flags 234 | } 235 | } 236 | 237 | func reachabilityChanged() { 238 | let block = connection != .none ? whenReachable : whenUnreachable 239 | 240 | DispatchQueue.main.async { [weak self] in 241 | guard let strongSelf = self else { return } 242 | block?(strongSelf) 243 | strongSelf.notificationCenter.post(name: .reachabilityChanged, object: strongSelf) 244 | } 245 | } 246 | } 247 | 248 | extension SCNetworkReachabilityFlags { 249 | 250 | typealias Connection = Reachability.Connection 251 | 252 | var connection: Connection { 253 | guard isReachableFlagSet else { return .none } 254 | 255 | // If we're reachable, but not on an iOS device (i.e. simulator), we must be on WiFi 256 | #if targetEnvironment(simulator) 257 | return .wifi 258 | #else 259 | var connection = Connection.none 260 | 261 | if !isConnectionRequiredFlagSet { 262 | connection = .wifi 263 | } 264 | 265 | if isConnectionOnTrafficOrDemandFlagSet { 266 | if !isInterventionRequiredFlagSet { 267 | connection = .wifi 268 | } 269 | } 270 | 271 | if isOnWWANFlagSet { 272 | connection = .cellular 273 | } 274 | 275 | return connection 276 | #endif 277 | } 278 | 279 | var isOnWWANFlagSet: Bool { 280 | #if os(iOS) 281 | return contains(.isWWAN) 282 | #else 283 | return false 284 | #endif 285 | } 286 | var isReachableFlagSet: Bool { 287 | return contains(.reachable) 288 | } 289 | var isConnectionRequiredFlagSet: Bool { 290 | return contains(.connectionRequired) 291 | } 292 | var isInterventionRequiredFlagSet: Bool { 293 | return contains(.interventionRequired) 294 | } 295 | var isConnectionOnTrafficFlagSet: Bool { 296 | return contains(.connectionOnTraffic) 297 | } 298 | var isConnectionOnDemandFlagSet: Bool { 299 | return contains(.connectionOnDemand) 300 | } 301 | var isConnectionOnTrafficOrDemandFlagSet: Bool { 302 | return !intersection([.connectionOnTraffic, .connectionOnDemand]).isEmpty 303 | } 304 | var isTransientConnectionFlagSet: Bool { 305 | return contains(.transientConnection) 306 | } 307 | var isLocalAddressFlagSet: Bool { 308 | return contains(.isLocalAddress) 309 | } 310 | var isDirectFlagSet: Bool { 311 | return contains(.isDirect) 312 | } 313 | var isConnectionRequiredAndTransientFlagSet: Bool { 314 | return intersection([.connectionRequired, .transientConnection]) == [.connectionRequired, .transientConnection] 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /RPiCameraViewer/Classes/Settings.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2016-2018 Shawn Baker using the MIT License. 2 | import Foundation 3 | 4 | class Settings: NSObject, NSCoding 5 | { 6 | // instance variables 7 | var cameraName = "camera".local 8 | var showAllCameras = false 9 | var scanTimeout = 500 10 | var port = 5001 11 | 12 | //********************************************************************** 13 | // init 14 | //********************************************************************** 15 | override init() 16 | { 17 | } 18 | 19 | //********************************************************************** 20 | // init 21 | //********************************************************************** 22 | required init(coder decoder: NSCoder) 23 | { 24 | cameraName = decoder.decodeObject(forKey: "cameraName") as! String 25 | showAllCameras = decoder.decodeBool(forKey: "showAllCameras") 26 | scanTimeout = decoder.decodeInteger(forKey: "scanTimeout") 27 | port = decoder.decodeInteger(forKey: "port") 28 | } 29 | 30 | //********************************************************************** 31 | // encode 32 | //********************************************************************** 33 | func encode(with encoder: NSCoder) 34 | { 35 | encoder.encode(cameraName, forKey: "cameraName") 36 | encoder.encode(showAllCameras, forKey: "showAllCameras") 37 | encoder.encode(scanTimeout, forKey: "scanTimeout") 38 | encoder.encode(port, forKey: "port") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /RPiCameraViewer/Classes/StringExt.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2016-2017 Shawn Baker using the MIT License. 2 | import Foundation 3 | import UIKit 4 | 5 | extension String 6 | { 7 | //********************************************************************** 8 | // length 9 | //********************************************************************** 10 | var length: Int 11 | { 12 | return self.count 13 | } 14 | 15 | //********************************************************************** 16 | // local 17 | //********************************************************************** 18 | var local: String 19 | { 20 | return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "") 21 | } 22 | 23 | //********************************************************************** 24 | // htmlAttr 25 | //********************************************************************** 26 | var htmlAttr: NSAttributedString? 27 | { 28 | let text = NSString(format:"%@", self) as String 29 | guard let data = text.data(using: String.Encoding.utf16, allowLossyConversion: false) else { return nil } 30 | //let attr = [NSAttributedString.DocumentAttributeKey.documentType: NSAttributedString.DocumentType.html] 31 | guard let html = try? NSMutableAttributedString(data: data, 32 | options: [.documentType : NSAttributedString.DocumentType.html], 33 | documentAttributes: nil) else { return nil } 34 | //guard let html = try? NSMutableAttributedString(data: data, options: attr, documentAttributes: nil) else { return nil } 35 | return html 36 | } 37 | 38 | //********************************************************************** 39 | // subscript integerIndex 40 | //********************************************************************** 41 | subscript(integerIndex: Int) -> Character 42 | { 43 | let i = index(startIndex, offsetBy: integerIndex) 44 | return self[i] 45 | } 46 | 47 | //********************************************************************** 48 | // subscript integerRange 49 | //********************************************************************** 50 | subscript(integerRange: Range) -> String 51 | { 52 | let start = index(startIndex, offsetBy: integerRange.lowerBound) 53 | let end = index(startIndex, offsetBy: integerRange.upperBound) 54 | return String(self[start.. String 21 | { 22 | var ssid = "" 23 | if let interfaces = CNCopySupportedInterfaces() as NSArray? 24 | { 25 | for interface in interfaces 26 | { 27 | if let interfaceInfo = CNCopyCurrentNetworkInfo(interface as! CFString) as NSDictionary? 28 | { 29 | ssid = interfaceInfo[kCNNetworkInfoKeySSID as String] as! String 30 | break 31 | } 32 | } 33 | } 34 | return ssid 35 | } 36 | 37 | //********************************************************************** 38 | // connectedToNetwork 39 | //********************************************************************** 40 | class func connectedToNetwork() -> Bool 41 | { 42 | let name = getNetworkName(); 43 | return !name.isEmpty; 44 | } 45 | 46 | //********************************************************************** 47 | // getNetworkInterfaces 48 | //********************************************************************** 49 | class func getNetworkInterfaces() -> [NetworkInterface] 50 | { 51 | var interfaces = [NetworkInterface]() 52 | var ifaddr: UnsafeMutablePointer? = nil 53 | if getifaddrs(&ifaddr) == 0 54 | { 55 | var ptr = ifaddr 56 | while ptr != nil 57 | { 58 | defer { ptr = ptr?.pointee.ifa_next } 59 | 60 | if let interface = ptr?.pointee 61 | { 62 | let family = interface.ifa_addr.pointee.sa_family 63 | if family == UInt8(AF_INET) || family == UInt8(AF_INET6) 64 | { 65 | let flags = interface.ifa_flags 66 | let name = String(cString: (interface.ifa_name)!) 67 | var cAddress = [CChar](repeating: 0, count: Int(NI_MAXHOST)) 68 | getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len), &cAddress, socklen_t(cAddress.count), nil, socklen_t(0), NI_NUMERICHOST) 69 | let address = String(cString: cAddress) 70 | var cNetmask = [CChar](repeating: 0, count: Int(NI_MAXHOST)) 71 | getnameinfo(interface.ifa_netmask, socklen_t(interface.ifa_netmask.pointee.sa_len), &cNetmask, socklen_t(cNetmask.count), nil, socklen_t(0), NI_NUMERICHOST) 72 | let netmask = String(cString: cNetmask) 73 | interfaces.append(NetworkInterface(name, flags, family, address, netmask)) 74 | } 75 | } 76 | } 77 | freeifaddrs(ifaddr) 78 | } 79 | return interfaces 80 | } 81 | 82 | //********************************************************************** 83 | // getWirelessAddress 84 | //********************************************************************** 85 | class func getWirelessInterface() -> NetworkInterface 86 | { 87 | let upAndRunning = UInt32(IFF_UP | IFF_RUNNING) 88 | let interfaces = getNetworkInterfaces() 89 | for interface in interfaces 90 | { 91 | if interface.name == "en0" && interface.family == UInt8(AF_INET) && 92 | (interface.flags & upAndRunning) == upAndRunning 93 | { 94 | return interface 95 | } 96 | } 97 | return NetworkInterface() 98 | } 99 | 100 | //********************************************************************** 101 | // getIPAddress 102 | //********************************************************************** 103 | class func getIPAddress() -> String 104 | { 105 | return getWirelessInterface().address 106 | } 107 | 108 | //********************************************************************** 109 | // getBaseIPAddress 110 | //********************************************************************** 111 | class func getBaseIPAddress(_ ipAddress: String) -> String 112 | { 113 | var address = ipAddress 114 | if !address.isEmpty, let i = address.range(of: ".", options: .backwards)?.lowerBound 115 | { 116 | address = String(address[...i]) 117 | } 118 | return address 119 | } 120 | 121 | //********************************************************************** 122 | // isIpAddress 123 | //********************************************************************** 124 | class func isIpAddress(_ address: String) -> Bool 125 | { 126 | let range = NSRange(location: 0, length: address.utf16.count) 127 | return ipAddressRegex.firstMatch(in: address, options: [], range: range) != nil 128 | } 129 | 130 | //********************************************************************** 131 | // isHostname 132 | //********************************************************************** 133 | class func isHostname(_ address: String) -> Bool 134 | { 135 | let range = NSRange(location: 0, length: address.utf16.count) 136 | return hostnameRegex.firstMatch(in: address, options: [], range: range) != nil 137 | } 138 | 139 | //********************************************************************** 140 | // resolveHostname 141 | //********************************************************************** 142 | class func resolveHostname(_ hostname: String) -> String 143 | { 144 | var address = "" 145 | let host = CFHostCreateWithName(nil, hostname as CFString).takeRetainedValue() 146 | CFHostStartInfoResolution(host, .addresses, nil) 147 | var success: DarwinBoolean = false 148 | if let addresses = CFHostGetAddressing(host, &success)?.takeUnretainedValue() as NSArray?, 149 | let addr = addresses.firstObject as? NSData 150 | { 151 | var name = [CChar](repeating: 0, count: Int(NI_MAXHOST)) 152 | if getnameinfo(addr.bytes.assumingMemoryBound(to: sockaddr.self), socklen_t(addr.length), 153 | &name, socklen_t(name.count), nil, 0, NI_NUMERICHOST) == 0 154 | { 155 | address = String(cString: name) 156 | } 157 | } 158 | return address 159 | } 160 | 161 | //********************************************************************** 162 | // getNetworkCameras 163 | //********************************************************************** 164 | class func getNetworkCameras(_ network: String, _ includeHostnames: Bool) -> [Camera] 165 | { 166 | var networkCameras = [Camera]() 167 | if !network.isEmpty 168 | { 169 | let app = UIApplication.shared.delegate as! AppDelegate 170 | for camera in app.cameras 171 | { 172 | let isIp = isIpAddress(camera.address) 173 | if (isIp && camera.network == network) || (!isIp && includeHostnames) 174 | { 175 | networkCameras.append(camera) 176 | } 177 | } 178 | } 179 | 180 | return networkCameras 181 | } 182 | 183 | //********************************************************************** 184 | // getDefaultCameraName 185 | //********************************************************************** 186 | class func getDefaultCameraName() -> String 187 | { 188 | let app = UIApplication.shared.delegate as! AppDelegate 189 | return app.settings.cameraName 190 | } 191 | 192 | //********************************************************************** 193 | // getMaxCameraNumber 194 | //********************************************************************** 195 | class func getMaxCameraNumber(_ cameras: [Camera]) -> Int 196 | { 197 | var max = 0 198 | let defaultName = getDefaultCameraName() + " " 199 | for camera in cameras 200 | { 201 | if camera.name.hasPrefix(defaultName) 202 | { 203 | let index = camera.name.index(camera.name.startIndex, offsetBy: defaultName.count) 204 | if let num = Int(camera.name[index...]), num > max 205 | { 206 | max = num 207 | } 208 | } 209 | } 210 | return max 211 | } 212 | 213 | //********************************************************************** 214 | // getNextCameraName 215 | //********************************************************************** 216 | class func getNextCameraName(_ cameras: [Camera]) -> String 217 | { 218 | return getDefaultCameraName() + " " + String(getMaxCameraNumber(cameras) + 1) 219 | } 220 | 221 | //********************************************************************** 222 | // getSnapshot 223 | //********************************************************************** 224 | class func getSnapshot(_ view: UIView) -> UIImage? 225 | { 226 | var image: UIImage? = nil 227 | UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0) 228 | view.drawHierarchy(in: view.bounds, afterScreenUpdates: true) 229 | image = UIGraphicsGetImageFromCurrentImageContext() 230 | UIGraphicsEndImageContext() 231 | return image; 232 | } 233 | 234 | //********************************************************************** 235 | // getIntTextField 236 | //********************************************************************** 237 | class func getIntTextField(_ vc: UIViewController, _ intField: IntTextField, _ name: String) -> Int? 238 | { 239 | // make sure there's a value 240 | guard let value = intField.value else 241 | { 242 | let message = String(format: "errorNoValue".local, name.local) 243 | error(vc, message) 244 | return nil 245 | } 246 | 247 | // make sure it's in range 248 | guard value >= intField.min && value <= intField.max else 249 | { 250 | let message = String(format: "errorValueOutOfRange".local, name.local, intField.min, intField.max) 251 | error(vc, message) 252 | return nil 253 | } 254 | 255 | // return the value 256 | return value 257 | } 258 | 259 | //********************************************************************** 260 | // error 261 | //********************************************************************** 262 | class func error(_ vc: UIViewController, _ message: String) 263 | { 264 | let alert = UIAlertController(title: "error".local, message: message.local, preferredStyle: UIAlertControllerStyle.alert) 265 | alert.addAction(UIAlertAction(title: "ok".local, style: UIAlertActionStyle.default, handler: nil)) 266 | vc.present(alert, animated: true, completion: nil) 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /RPiCameraViewer/Classes/ZoomPan.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Shawn Baker using the MIT License. 2 | import Foundation 3 | import UIKit 4 | 5 | class ZoomPan 6 | { 7 | // instance variables 8 | var view: UIView 9 | var minZoom: CGFloat = 1 10 | var maxZoom: CGFloat = 10.0 11 | var videoSize = CGSize.zero 12 | var fitSize = CGSize.zero 13 | var zoom: CGFloat = 1 14 | var pan = CGPoint.zero 15 | var panStart = CGPoint.zero 16 | 17 | //********************************************************************** 18 | // init 19 | //********************************************************************** 20 | init(_ view: UIView) 21 | { 22 | self.view = view 23 | } 24 | 25 | //********************************************************************** 26 | // reset 27 | //********************************************************************** 28 | func reset() 29 | { 30 | // get the fitted view size 31 | let viewSize = view.bounds.size 32 | let viewAspect = viewSize.height / viewSize.width 33 | let videoAspect = videoSize.height / videoSize.width 34 | if videoAspect < viewAspect 35 | { 36 | fitSize.width = viewSize.width 37 | fitSize.height = videoSize.height * viewSize.width / videoSize.width 38 | } 39 | else 40 | { 41 | fitSize.width = videoSize.width * viewSize.height / videoSize.height 42 | fitSize.height = viewSize.height 43 | } 44 | 45 | // initialize the zoom and pan 46 | setZoomPan(zoom, pan) 47 | } 48 | 49 | //********************************************************************** 50 | // setVideoSize 51 | //********************************************************************** 52 | func setVideoSize(size: CGSize) 53 | { 54 | if size != videoSize 55 | { 56 | // set the video size 57 | videoSize = size; 58 | 59 | // reset the view 60 | reset() 61 | } 62 | } 63 | 64 | //********************************************************************** 65 | // setVideoSize 66 | //********************************************************************** 67 | func setVideoSize(_ width: CGFloat, _ height: CGFloat) 68 | { 69 | setVideoSize(size: CGSize(width: width, height: height)) 70 | } 71 | 72 | //********************************************************************** 73 | // setZoom 74 | //********************************************************************** 75 | func setZoom(_ zoom: CGFloat) 76 | { 77 | self.zoom = max(minZoom, min(zoom, maxZoom)) 78 | checkPan() 79 | setTransform() 80 | } 81 | 82 | //********************************************************************** 83 | // setPan 84 | //********************************************************************** 85 | func setPan(_ pan: CGPoint) 86 | { 87 | self.pan = pan 88 | checkPan() 89 | setTransform() 90 | } 91 | 92 | //********************************************************************** 93 | // setPan 94 | //********************************************************************** 95 | func setPan(_ x: CGFloat, _ y: CGFloat) 96 | { 97 | setPan(CGPoint(x: x, y: y)) 98 | } 99 | 100 | //********************************************************************** 101 | // setZoomPan 102 | //********************************************************************** 103 | func setZoomPan(_ zoom: CGFloat, _ pan: CGPoint) 104 | { 105 | self.zoom = max(minZoom, min(zoom, maxZoom)) 106 | self.pan = pan 107 | checkPan() 108 | setTransform() 109 | } 110 | 111 | //********************************************************************** 112 | // setZoomPan 113 | //********************************************************************** 114 | func setZoomPan(_ zoom: CGFloat, _ panX: CGFloat, _ panY: CGFloat) 115 | { 116 | setZoomPan(zoom, CGPoint(x: panX, y: panY)) 117 | } 118 | 119 | //********************************************************************** 120 | // checkPan 121 | //********************************************************************** 122 | private func checkPan() 123 | { 124 | let maxPan = getMaxPan() 125 | 126 | if maxPan.x == 0 { pan.x = 0 } 127 | else if pan.x < -maxPan.x { pan.x = -maxPan.x } 128 | else if pan.x > maxPan.x { pan.x = maxPan.x } 129 | 130 | if maxPan.y == 0 { pan.y = 0 } 131 | else if pan.y < -maxPan.y { pan.y = -maxPan.y } 132 | else if pan.y > maxPan.y { pan.y = maxPan.y } 133 | } 134 | 135 | //********************************************************************** 136 | // getMaxPan 137 | //********************************************************************** 138 | private func getMaxPan() -> CGPoint 139 | { 140 | let maxPan = CGPoint(x: max((fitSize.width * zoom - view.bounds.width) / 2 / zoom, 0), 141 | y: max((fitSize.height * zoom - view.bounds.height) / 2 / zoom, 0)) 142 | return maxPan 143 | } 144 | 145 | //********************************************************************** 146 | // setTransform 147 | //********************************************************************** 148 | private func setTransform() 149 | { 150 | view.transform = CGAffineTransform.init(scaleX: zoom, y: zoom).translatedBy(x: pan.x, y: pan.y) 151 | } 152 | 153 | //********************************************************************** 154 | // handlePinchGesture 155 | //********************************************************************** 156 | @objc func handlePinchGesture(_ gesture: UIPinchGestureRecognizer) 157 | { 158 | var newZoom = zoom * gesture.scale 159 | newZoom = max(minZoom, min(newZoom, maxZoom)) 160 | if newZoom != zoom 161 | { 162 | let diff = (newZoom / zoom) - 1 163 | let location = gesture.location(in: view) 164 | let offset = CGPoint(x: location.x - view.bounds.midX + pan.x, y: location.y - view.bounds.midY + pan.y) 165 | setZoomPan(newZoom, pan.x - offset.x * diff, pan.y - offset.y * diff) 166 | } 167 | gesture.scale = 1 168 | } 169 | 170 | //********************************************************************** 171 | // handlePanGesture 172 | //********************************************************************** 173 | @objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) 174 | { 175 | if gesture.state == UIGestureRecognizerState.began 176 | { 177 | panStart = pan 178 | } 179 | if (zoom > 1) 180 | { 181 | let distance = gesture.translation(in: view) 182 | setPan(panStart.x + distance.x, panStart.y + distance.y) 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /RPiCameraViewer/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | RPi Viewer 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.1 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSPhotoLibraryAddUsageDescription 26 | Save snapshot. 27 | NSPhotoLibraryUsageDescription 28 | Allows the app to save your snapshots to the photo library. 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UIRequiredDeviceCapabilities 34 | 35 | armv7 36 | 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | UIInterfaceOrientationPortraitUpsideDown 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | UIViewControllerBasedStatusBarAppearance 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /RPiCameraViewer/Localizable.strings: -------------------------------------------------------------------------------- 1 | // Copyright © 2016-2019 Shawn Baker using the MIT License. 2 | errorBadAddress = "The address is not a valid IP address or host name."; 3 | errorCouldntResolveHostname = "Couldn't resolve hostname\n%@"; 4 | errorNameAlreadyExists = "The camera name already exists."; 5 | errorNoAddress = "You must enter an address."; 6 | errorNoCamera = "No camera object."; 7 | errorNoName = "You must enter a name."; 8 | errorNoNetwork = "There is no network to scan."; 9 | errorNotAuthorizedToSavePhotos = "This app is not authorized to save photos to the Photo Library."; 10 | errorNoValue = "You must enter a %@."; 11 | errorValueOutOfRange = "The %@ must be between %d and %d."; 12 | 13 | aboutInfo = "Copyright © 2016-2019 Shawn Baker

14 | This is an open source program.
15 | It\'s available on github under the MIT License

16 | It uses the Reachability.swift class to detect network changes.

17 | Raspberry image by Martin Bérubé
18 | Camera image by Oxygen Team
19 | Tab bar icons by icons8
"; 20 | appName = "RPi Camera Viewer"; 21 | bps = "bits per second"; 22 | camera = "Camera"; 23 | deleteAllCameras = "Delete all Cameras"; 24 | done = "Done"; 25 | error = "Error"; 26 | fps = "frames per second"; 27 | height = "height"; 28 | helpText = "

This program plays the raw H.264 video from a Raspberry Pi. 29 |

30 | Use the raspivid program to generate the video stream and the nc program to send the stream out over a specific port. 31 |

32 | For example, to stream 1280x720 video at 15 frames per second over port 5001, you would do: 33 |

34 | raspivid -n -ih -t 0 -rot 0 -w 1280 -h 720 -b 1000000 -fps 15 -o - | nc -lkv4 5001

"; 35 | newCamerasFound = "New cameras found: %d"; 36 | initializingVideo = "Initializing video..."; 37 | no = "No"; 38 | notScanning = "Not scanning."; 39 | ok = "OK"; 40 | okToDeleteAllCameras = "Are you sure you want to delete all the cameras?"; 41 | port = "port"; 42 | scanningOnPort = "Scanning on port %d"; 43 | scanTimeout = "scan timeout"; 44 | version = "Version %@"; 45 | versionBuild = "Version %@ (build %@)"; 46 | videoStopped = "Video stopped."; 47 | width = "width"; 48 | yes = "Yes"; 49 | -------------------------------------------------------------------------------- /RPiCameraViewer/RPiCameraViewer.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.networking.wifi-info 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RPiCameraViewer/Sockets/mysocket.c: -------------------------------------------------------------------------------- 1 | // Copyright © 2016-2017 Shawn Baker using the MIT License. 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "mysocket.h" 11 | 12 | //********************************************************************** 13 | // openSocket 14 | //********************************************************************** 15 | int openSocket(const char *address, int port, int timeout) 16 | { 17 | struct sockaddr_in serv_addr; 18 | fd_set fdset; 19 | struct timeval tv; 20 | 21 | // set the connection parameters 22 | memset(&serv_addr, '0', sizeof(serv_addr)); 23 | serv_addr.sin_family = AF_INET; 24 | serv_addr.sin_port = htons(port); 25 | if (inet_pton(AF_INET, address, &serv_addr.sin_addr) <= 0) 26 | { 27 | return -1; 28 | } 29 | 30 | // open the socket and make it non-blocking 31 | int fd = socket(AF_INET, SOCK_STREAM, 0); 32 | if (fd < 0) 33 | { 34 | return -2; 35 | } 36 | int flags = fcntl(fd, F_GETFL, 0); 37 | fcntl(fd, F_SETFL, flags | O_NONBLOCK); 38 | 39 | // connect the socket 40 | if (connect(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0 && errno != EINPROGRESS) 41 | { 42 | close(fd); 43 | return -3; 44 | } 45 | 46 | // wait for the connect to succeed or timeout 47 | FD_ZERO(&fdset); 48 | FD_SET(fd, &fdset); 49 | tv.tv_sec = 0; 50 | tv.tv_usec = timeout * 1000; 51 | if (select(fd + 1, NULL, &fdset, NULL, &tv) <= 0) 52 | { 53 | close(fd); 54 | return -4; 55 | } 56 | int err; 57 | socklen_t errlen = sizeof(err); 58 | getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen); 59 | if (err != 0) 60 | { 61 | close(fd); 62 | return -5; 63 | } 64 | 65 | // set the socket back to blocking 66 | fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); 67 | 68 | // return the file 69 | return fd; 70 | } 71 | 72 | //********************************************************************** 73 | // closeSocket 74 | //********************************************************************** 75 | void closeSocket(int fd) 76 | { 77 | close(fd); 78 | } 79 | 80 | //********************************************************************** 81 | // readSocket 82 | //********************************************************************** 83 | int readSocket(int fd, unsigned char *buffer, int len) 84 | { 85 | int n; 86 | do 87 | { 88 | n = (int)read(fd, buffer, len); 89 | } 90 | while (n == -1 && errno == EINTR); 91 | return n; 92 | } 93 | -------------------------------------------------------------------------------- /RPiCameraViewer/Sockets/mysocket.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2016-2017 Shawn Baker using the MIT License. 2 | #ifndef socket_h 3 | #define socket_h 4 | 5 | #include 6 | 7 | int openSocket(const char *address, int port, int timeout); 8 | void closeSocket(int fd); 9 | int readSocket(int fd, unsigned char *buffer, int len); 10 | 11 | #endif /* socket_h */ 12 | -------------------------------------------------------------------------------- /RPiCameraViewer/ViewControllers/AboutViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2016-2019 Shawn Baker using the MIT License. 2 | import UIKit 3 | 4 | class AboutViewController: UIViewController 5 | { 6 | // outlets 7 | @IBOutlet weak var appNameLabel: UILabel! 8 | @IBOutlet weak var versionLabel: UILabel! 9 | @IBOutlet weak var infoTextView: UITextView! 10 | 11 | //********************************************************************** 12 | // viewDidLoad 13 | //********************************************************************** 14 | override func viewDidLoad() 15 | { 16 | super.viewDidLoad() 17 | 18 | appNameLabel.text = "appName".local 19 | if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, 20 | let buildStr = Bundle.main.infoDictionary?["CFBundleVersion"] as? String, 21 | let build = Int(buildStr) 22 | { 23 | let format = (build > 1) ? "versionBuild" : "version" 24 | versionLabel.text = String(format: format.local, version, buildStr) 25 | } 26 | infoTextView.attributedText = "aboutInfo".local.htmlAttr 27 | } 28 | 29 | //********************************************************************** 30 | // viewDidLayoutSubviews 31 | //********************************************************************** 32 | override func viewDidLayoutSubviews() 33 | { 34 | infoTextView.contentOffset = CGPoint.zero 35 | super.viewDidLayoutSubviews() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /RPiCameraViewer/ViewControllers/CameraViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2016-2019 Shawn Baker using the MIT License. 2 | import UIKit 3 | 4 | class CameraViewController: InputViewController 5 | { 6 | // outlets 7 | @IBOutlet weak var mainScrollViewBottomConstraint: NSLayoutConstraint! 8 | @IBOutlet weak var mainScrollView: UIScrollView! 9 | @IBOutlet weak var networkLabel: UILabel! 10 | @IBOutlet weak var nameTextField: UITextField! 11 | @IBOutlet weak var addressTextField: UITextField! 12 | @IBOutlet weak var portIntField: IntTextField! 13 | 14 | // instance variables 15 | var camera = Camera() 16 | 17 | //********************************************************************** 18 | // viewDidLoad 19 | //********************************************************************** 20 | override func viewDidLoad() 21 | { 22 | super.viewDidLoad() 23 | 24 | initScrollView(mainScrollView, mainScrollViewBottomConstraint) 25 | 26 | networkLabel.text = camera.network 27 | nameTextField.text = camera.name 28 | addressTextField.text = camera.address 29 | portIntField.value = camera.port 30 | } 31 | 32 | //********************************************************************** 33 | // textFieldShouldReturn 34 | //********************************************************************** 35 | func textFieldShouldReturn(_ textField: UITextField) -> Bool 36 | { 37 | if textField == nameTextField || textField == addressTextField 38 | { 39 | textField.resignFirstResponder() 40 | } 41 | return true 42 | } 43 | 44 | // MARK: - Navigation 45 | 46 | //********************************************************************** 47 | // shouldPerformSegue 48 | //********************************************************************** 49 | override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool 50 | { 51 | if identifier == "SaveCamera" 52 | { 53 | // error check the input values 54 | guard let name = nameTextField.text, name.length > 0 else 55 | { 56 | Utils.error(self, "errorNoName") 57 | return false 58 | } 59 | guard name == camera.name || !app.cameras.contains(where: {$0.name == name}) else 60 | { 61 | Utils.error(self, "errorNameAlreadyExists") 62 | return false 63 | } 64 | guard let address = addressTextField.text, address.length > 0 else 65 | { 66 | Utils.error(self, "errorNoAddress") 67 | return false 68 | } 69 | guard Utils.isIpAddress(address) || Utils.isHostname(address) else 70 | { 71 | Utils.error(self, "errorBadAddress") 72 | return false 73 | } 74 | guard let port = Utils.getIntTextField(self, portIntField, "port") 75 | else 76 | { 77 | return false 78 | } 79 | 80 | // assign the new values to the camera 81 | camera.name = name 82 | //camera.network = networkLabel.text! 83 | camera.address = address 84 | camera.port = port 85 | } 86 | return true 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /RPiCameraViewer/ViewControllers/CamerasViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2016-2019 Shawn Baker using the MIT License. 2 | import UIKit 3 | 4 | class CamerasViewController: UIViewController, UITableViewDataSource, UITableViewDelegate 5 | { 6 | // outlets 7 | @IBOutlet weak var tableView: UITableView! 8 | 9 | // constants 10 | let SCAN_TIMEOUT = 0.5 11 | 12 | // instance variables 13 | let cameraCellId = "CameraCell" 14 | let emptyCellId = "EmptyCameraCell" 15 | let app = UIApplication.shared.delegate as! AppDelegate 16 | let reachability = Reachability()! 17 | var camera = Camera() 18 | var cameras = [Camera]() 19 | var showNetwork = false 20 | 21 | //********************************************************************** 22 | // viewDidLoad 23 | //********************************************************************** 24 | override func viewDidLoad() 25 | { 26 | // initialize the views 27 | super.viewDidLoad() 28 | tableView.delegate = self 29 | tableView.dataSource = self 30 | 31 | // get the list of cameras 32 | refreshCameras() 33 | 34 | // detect network changes 35 | reachability.whenReachable = { reachability in 36 | self.refreshCameras() 37 | } 38 | reachability.whenUnreachable = { _ in 39 | self.refreshCameras() 40 | } 41 | 42 | // if there are no cameras then do a scan 43 | if cameras.count == 0 44 | { 45 | DispatchQueue.main.asyncAfter(deadline: .now() + self.SCAN_TIMEOUT, execute: 46 | { 47 | self.performSegue(withIdentifier: "ScanForCameras", sender: self) 48 | }) 49 | } 50 | } 51 | 52 | //********************************************************************** 53 | // viewWillAppear 54 | //********************************************************************** 55 | override func viewWillAppear(_ animated: Bool) 56 | { 57 | super.viewWillAppear(animated) 58 | try? reachability.startNotifier() 59 | } 60 | 61 | //********************************************************************** 62 | // viewWillDisappear 63 | //********************************************************************** 64 | override func viewWillDisappear(_ animated: Bool) 65 | { 66 | super.viewWillDisappear(animated) 67 | reachability.stopNotifier() 68 | } 69 | 70 | //********************************************************************** 71 | // didReceiveMemoryWarning 72 | //********************************************************************** 73 | override func didReceiveMemoryWarning() 74 | { 75 | super.didReceiveMemoryWarning() 76 | } 77 | 78 | //********************************************************************** 79 | // cancelCamera 80 | //********************************************************************** 81 | @IBAction func cancelCamera(segue:UIStoryboardSegue) 82 | { 83 | } 84 | 85 | //********************************************************************** 86 | // saveCamera 87 | //********************************************************************** 88 | @IBAction func saveCamera(segue:UIStoryboardSegue) 89 | { 90 | if segue.identifier == "SaveCamera", 91 | let vc = segue.source as? CameraViewController 92 | { 93 | // update the global list of cameras 94 | if let i = app.cameras.index(of: camera) 95 | { 96 | app.cameras.remove(at: i) 97 | } 98 | app.cameras.append(vc.camera) 99 | app.cameras = app.cameras.sorted(by: { $0.name < $1.name }) 100 | app.save() 101 | 102 | // refresh the local list of cameras 103 | refreshCameras() 104 | } 105 | } 106 | 107 | //********************************************************************** 108 | // getCameras 109 | //********************************************************************** 110 | func getCameras() 111 | { 112 | var showAllCameras = !Utils.connectedToNetwork() || app.settings.showAllCameras 113 | if showAllCameras 114 | { 115 | cameras = app.cameras 116 | } 117 | else 118 | { 119 | let network = Utils.getNetworkName() 120 | showAllCameras = network.isEmpty 121 | cameras = showAllCameras ? app.cameras : Utils.getNetworkCameras(network, true) 122 | } 123 | cameras.sort(by: { $0.name < $1.name }) 124 | showNetwork = showAllCameras 125 | } 126 | 127 | //********************************************************************** 128 | // refreshCameras 129 | //********************************************************************** 130 | func refreshCameras() 131 | { 132 | getCameras() 133 | tableView.reloadData() 134 | } 135 | 136 | //********************************************************************** 137 | // updateCameras 138 | //********************************************************************** 139 | @IBAction func updateCameras(segue:UIStoryboardSegue) 140 | { 141 | refreshCameras() 142 | } 143 | 144 | //********************************************************************** 145 | // deleteAllCameras 146 | //********************************************************************** 147 | @IBAction func deleteAllCameras(_ sender: UIBarButtonItem) 148 | { 149 | let alert = UIAlertController(title: "deleteAllCameras".local, message: "okToDeleteAllCameras".local, preferredStyle: .alert) 150 | 151 | alert.addAction(UIAlertAction(title: "yes".local, style: .default) { (action:UIAlertAction) in 152 | 153 | // remove the cameras from the global list of cameras 154 | for camera in self.cameras 155 | { 156 | if let i = self.app.cameras.index(of: camera) 157 | { 158 | self.app.cameras.remove(at: i) 159 | } 160 | } 161 | 162 | // refresh the local list of cameras 163 | self.refreshCameras() 164 | }) 165 | alert.addAction(UIAlertAction(title: "no".local, style: .default)) 166 | 167 | self.present(alert, animated: true, completion: nil) 168 | } 169 | 170 | // MARK: - Navigation 171 | 172 | //********************************************************************** 173 | // prepare 174 | //********************************************************************** 175 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) 176 | { 177 | if let vc = segue.destination as? CameraViewController 178 | { 179 | if segue.identifier == "CreateCamera" 180 | { 181 | vc.camera = Camera(Utils.getNextCameraName(app.cameras), Utils.getNetworkName(), 182 | Utils.getBaseIPAddress(Utils.getIPAddress()), app.settings.port) 183 | self.camera = vc.camera 184 | } 185 | else if segue.identifier == "EditCamera", let camera = sender as? Camera 186 | { 187 | vc.camera = Camera(camera) 188 | self.camera = camera 189 | } 190 | } 191 | else if let vc = segue.destination as? VideoViewController 192 | { 193 | if segue.identifier == "ShowVideo", let camera = sender as? Camera 194 | { 195 | vc.camera = Camera(camera) 196 | } 197 | } 198 | } 199 | 200 | // MARK: UITableViewDataSource Methods 201 | 202 | //********************************************************************** 203 | // tableView numberOfRowsInSection 204 | //********************************************************************** 205 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 206 | { 207 | return cameras.count 208 | } 209 | 210 | //********************************************************************** 211 | // tableView cellForRowAt 212 | //********************************************************************** 213 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 214 | { 215 | let cell = tableView.dequeueReusableCell(withIdentifier: cameraCellId, for: indexPath) 216 | let camera = cameras[indexPath.row] 217 | cell.textLabel?.text = camera.name 218 | var details = camera.address + ":" + String(camera.port) 219 | if showNetwork && Utils.isIpAddress(camera.address) 220 | { 221 | details = camera.network + ":" + details 222 | } 223 | cell.detailTextLabel?.text = details 224 | return cell 225 | } 226 | 227 | //********************************************************************** 228 | // tableView commit 229 | //********************************************************************** 230 | func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) 231 | { 232 | if editingStyle == .delete 233 | { 234 | // remove the camera from the global list of cameras 235 | let camera = cameras[indexPath.row] 236 | if let i = app.cameras.index(of: camera) 237 | { 238 | app.cameras.remove(at: i) 239 | } 240 | app.save() 241 | 242 | // refresh the local list of cameras 243 | refreshCameras() 244 | } 245 | } 246 | 247 | // MARK: UITableViewDelegate Methods 248 | 249 | //********************************************************************** 250 | // tableView didSelectRowAt 251 | //********************************************************************** 252 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) 253 | { 254 | let camera = cameras[indexPath.row] 255 | performSegue(withIdentifier: "ShowVideo", sender: camera) 256 | } 257 | 258 | //********************************************************************** 259 | // tableView accessoryButtonTappedForRowWith 260 | //********************************************************************** 261 | func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) 262 | { 263 | let camera = cameras[indexPath.row] 264 | performSegue(withIdentifier: "EditCamera", sender: camera) 265 | } 266 | } 267 | 268 | -------------------------------------------------------------------------------- /RPiCameraViewer/ViewControllers/HelpViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2016-2019 Shawn Baker using the MIT License. 2 | import UIKit 3 | 4 | class HelpViewController: UIViewController 5 | { 6 | // outlets 7 | @IBOutlet weak var helpTextView: UITextView! 8 | 9 | //********************************************************************** 10 | // viewDidLoad 11 | //********************************************************************** 12 | override func viewDidLoad() 13 | { 14 | super.viewDidLoad() 15 | 16 | helpTextView.attributedText = "helpText".local.htmlAttr 17 | } 18 | 19 | //********************************************************************** 20 | // viewDidLayoutSubviews 21 | //********************************************************************** 22 | override func viewDidLayoutSubviews() 23 | { 24 | helpTextView.contentOffset = CGPoint.zero 25 | super.viewDidLayoutSubviews() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /RPiCameraViewer/ViewControllers/InputViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Shawn Baker using the MIT License. 2 | import UIKit 3 | 4 | class InputViewController: UIViewController, UITextViewDelegate, UITextFieldDelegate 5 | { 6 | var activeFieldRect: CGRect? 7 | var keyboardRect: CGRect? 8 | var scrollView: UIScrollView! 9 | var bottomConstraint: NSLayoutConstraint! 10 | var registered = false 11 | let app = UIApplication.shared.delegate as! AppDelegate 12 | 13 | //********************************************************************** 14 | // initScrollView 15 | //********************************************************************** 16 | func initScrollView(_ scrollView: UIScrollView, _ bottomConstraint: NSLayoutConstraint) 17 | { 18 | // configure the view controller 19 | automaticallyAdjustsScrollViewInsets = false 20 | 21 | // set the scroll view and bottom constraint 22 | self.scrollView = scrollView 23 | self.bottomConstraint = bottomConstraint 24 | 25 | // register for keyboard notifications 26 | registerForKeyboardNotifications() 27 | 28 | // set self as the delegate for the keyboard input fields 29 | for view in scrollView.subviews 30 | { 31 | if view is UITextView 32 | { 33 | let tv = view as! UITextView 34 | tv.delegate = self 35 | } 36 | else if view is UITextField 37 | { 38 | let tf = view as! UITextField 39 | tf.delegate = self 40 | } 41 | } 42 | } 43 | 44 | //********************************************************************** 45 | // viewDidLayoutSubviews 46 | //********************************************************************** 47 | override func viewDidLayoutSubviews() 48 | { 49 | super.viewDidLayoutSubviews() 50 | updateContentSize() 51 | } 52 | 53 | //********************************************************************** 54 | // deinit 55 | //********************************************************************** 56 | deinit 57 | { 58 | self.deregisterFromKeyboardNotifications() 59 | } 60 | 61 | //********************************************************************** 62 | // registerForKeyboardNotifications 63 | //********************************************************************** 64 | func registerForKeyboardNotifications() 65 | { 66 | if !registered 67 | { 68 | registered = true 69 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil) 70 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil) 71 | } 72 | } 73 | 74 | //********************************************************************** 75 | // deregisterFromKeyboardNotifications 76 | //********************************************************************** 77 | func deregisterFromKeyboardNotifications() 78 | { 79 | if registered 80 | { 81 | registered = false 82 | NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillShow, object: nil) 83 | NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillHide, object: nil) 84 | } 85 | } 86 | 87 | //********************************************************************** 88 | // keyboardWillShow 89 | //********************************************************************** 90 | @objc func keyboardWillShow(notification: NSNotification) 91 | { 92 | var info = notification.userInfo! 93 | keyboardRect = (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue 94 | adjustForKeyboard() 95 | } 96 | 97 | //********************************************************************** 98 | // keyboardWillHide 99 | //********************************************************************** 100 | @objc func keyboardWillHide(notification: NSNotification) 101 | { 102 | keyboardRect = nil 103 | adjustForKeyboard() 104 | } 105 | 106 | //********************************************************************** 107 | // adjustForKeyboard 108 | //********************************************************************** 109 | func adjustForKeyboard() 110 | { 111 | if scrollView != nil 112 | { 113 | if keyboardRect != nil 114 | { 115 | bottomConstraint.constant = bottomLayoutGuide.length - (keyboardRect?.height)!; 116 | if activeFieldRect != nil 117 | { 118 | scrollView.scrollRectToVisible(activeFieldRect!, animated: true) 119 | } 120 | } 121 | else 122 | { 123 | bottomConstraint.constant = 0 124 | } 125 | } 126 | } 127 | 128 | //********************************************************************** 129 | // textViewDidBeginEditing 130 | //********************************************************************** 131 | func textViewDidBeginEditing(_ textView: UITextView) 132 | { 133 | activeFieldRect = textView.frame 134 | adjustForKeyboard() 135 | } 136 | 137 | //********************************************************************** 138 | // textViewDidEndEditing 139 | //********************************************************************** 140 | func textViewDidEndEditing(_ textView: UITextView) 141 | { 142 | activeFieldRect = nil 143 | adjustForKeyboard() 144 | } 145 | 146 | //********************************************************************** 147 | // textFieldDidBeginEditing 148 | //********************************************************************** 149 | func textFieldDidBeginEditing(_ textField: UITextField) 150 | { 151 | activeFieldRect = textField.frame 152 | adjustForKeyboard() 153 | } 154 | 155 | //********************************************************************** 156 | // textFieldDidEndEditing 157 | //********************************************************************** 158 | func textFieldDidEndEditing(_ textField: UITextField) 159 | { 160 | activeFieldRect = nil 161 | adjustForKeyboard() 162 | } 163 | 164 | //********************************************************************** 165 | // updateContentSize 166 | //********************************************************************** 167 | func updateContentSize() 168 | { 169 | if scrollView != nil 170 | { 171 | let showsVerticalScrollIndicator = scrollView.showsVerticalScrollIndicator 172 | let showsHorizontalScrollIndicator = scrollView.showsHorizontalScrollIndicator 173 | 174 | scrollView.showsVerticalScrollIndicator = false 175 | scrollView.showsHorizontalScrollIndicator = false; 176 | 177 | var contentRect: CGRect = CGRect.zero 178 | if scrollView.subviews.count > 0 179 | { 180 | var origin = scrollView.subviews[0].frame.origin 181 | var max = CGPoint(x: scrollView.subviews[0].frame.maxX, y: scrollView.subviews[0].frame.maxY) 182 | for view in scrollView.subviews 183 | { 184 | if !view.isHidden 185 | { 186 | if view.frame.origin.x < origin.x { origin.x = view.frame.origin.x } 187 | if view.frame.origin.y < origin.y { origin.y = view.frame.origin.y } 188 | if view.frame.maxX > max.x { max.x = view.frame.maxX } 189 | if view.frame.maxY > max.y { max.y = view.frame.maxY } 190 | } 191 | } 192 | contentRect = CGRect(x: origin.x, y: origin.y, width: max.x - origin.x, height: max.y - origin.y) 193 | } 194 | 195 | scrollView.showsVerticalScrollIndicator = showsVerticalScrollIndicator 196 | scrollView.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator 197 | 198 | scrollView.contentSize = CGSize(width: contentRect.width + contentRect.origin.x * 2, 199 | height: contentRect.height + contentRect.origin.y * 2) 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /RPiCameraViewer/ViewControllers/ScanningViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017-2018 Shawn Baker using the MIT License. 2 | import UIKit 3 | 4 | class ScanningViewController: UIViewController 5 | { 6 | // outlets 7 | @IBOutlet weak var messageLabel: UILabel! 8 | @IBOutlet weak var progressView: UIProgressView! 9 | @IBOutlet weak var statusLabel: UILabel! 10 | @IBOutlet weak var cancelButton: Button! 11 | 12 | // constants 13 | let NO_DEVICE = -1 14 | let NUM_THREADS = 40 15 | let DISMISS_TIMEOUT = 1.5 16 | let app = UIApplication.shared.delegate as! AppDelegate 17 | let semaphore = DispatchSemaphore(value: 1) 18 | 19 | // variables 20 | var network = Utils.getNetworkName() 21 | var wireless = Utils.getWirelessInterface() 22 | var device = 0 23 | var numDone = 0 24 | var newCameras = [Camera]() 25 | var scanning = true 26 | 27 | //********************************************************************** 28 | // viewDidLoad 29 | //********************************************************************** 30 | override func viewDidLoad() 31 | { 32 | super.viewDidLoad() 33 | 34 | progressView.progress = 0 35 | progressView.transform = progressView.transform.scaledBy(x: 1, y: 2) 36 | cancelButton.addTarget(self, action:#selector(handleCancelButtonTouchUpInside), for: .touchUpInside) 37 | if network.isEmpty || wireless.address.isEmpty 38 | { 39 | messageLabel.text = "notScanning".local 40 | messageLabel.textColor = Utils.badTextColor 41 | statusLabel.text = "errorNoNetwork".local 42 | statusLabel.textColor = Utils.badTextColor 43 | cancelButton.setTitle("done".local, for: UIControlState.normal) 44 | } 45 | else 46 | { 47 | messageLabel.text = String(format: "scanningOnPort".local, app.settings.port) 48 | statusLabel.text = String(format: "newCamerasFound".local, 0) 49 | let baseAddress = Utils.getBaseIPAddress(wireless.address) 50 | for _ in 1...NUM_THREADS 51 | { 52 | DispatchQueue.global(qos: .background).async 53 | { 54 | var dev = self.getNextDevice() 55 | while self.scanning && dev != self.NO_DEVICE 56 | { 57 | let address = baseAddress + String(dev) 58 | if address != self.wireless.address 59 | { 60 | let socket = openSocket(address, Int32(self.app.settings.port), Int32(self.app.settings.scanTimeout)) 61 | if (socket >= 0) 62 | { 63 | self.addCamera(address) 64 | closeSocket(socket) 65 | } 66 | } 67 | self.doneDevice(dev) 68 | dev = self.getNextDevice() 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | //********************************************************************** 76 | // handleCancelButtonTouchUpInside 77 | //********************************************************************** 78 | @objc func handleCancelButtonTouchUpInside(_ sender: UIButton) 79 | { 80 | scanning = false 81 | self.performSegue(withIdentifier: "UpdateCameras", sender: self) 82 | } 83 | 84 | //********************************************************************** 85 | // getNextDevice 86 | //********************************************************************** 87 | func getNextDevice() -> Int 88 | { 89 | var nextDevice = NO_DEVICE 90 | semaphore.wait() 91 | if device < 254 92 | { 93 | device += 1 94 | nextDevice = device 95 | } 96 | semaphore.signal() 97 | return nextDevice 98 | } 99 | 100 | //********************************************************************** 101 | // doneDevice 102 | //********************************************************************** 103 | func doneDevice(_ device: Int) 104 | { 105 | semaphore.wait() 106 | numDone += 1 107 | setStatus(numDone == 254) 108 | semaphore.signal() 109 | } 110 | 111 | //********************************************************************** 112 | // addCamera 113 | //********************************************************************** 114 | func addCamera(_ address: String) 115 | { 116 | semaphore.wait() 117 | var found = false 118 | for camera in self.app.cameras 119 | { 120 | if camera.network == self.network && camera.address == address && camera.port == self.app.settings.port 121 | { 122 | found = true 123 | break 124 | } 125 | } 126 | if !found 127 | { 128 | //Log.info("addCamera: " + newCamera.source.toString()) 129 | let camera = Camera("", self.network, address, app.settings.port) 130 | self.newCameras.append(camera) 131 | } 132 | semaphore.signal() 133 | } 134 | 135 | //********************************************************************** 136 | // addCameras 137 | //********************************************************************** 138 | func addCameras() 139 | { 140 | if newCameras.count > 0 141 | { 142 | // sort the new cameras by IP address 143 | //Log.info("addCameras") 144 | newCameras.sort(by: compareCameras) 145 | 146 | // get the maximum number from the existing camera names 147 | var max = Utils.getMaxCameraNumber(app.cameras) 148 | 149 | // set the camera names and add the new cameras to the list of all cameras 150 | let defaultName = app.settings.cameraName + " " 151 | for camera in newCameras 152 | { 153 | max += 1 154 | camera.name = defaultName + String(max) 155 | app.cameras.append(camera) 156 | //Log.info("camera: " + camera.toString()) 157 | } 158 | 159 | app.save() 160 | } 161 | } 162 | 163 | //********************************************************************** 164 | // compareCameras 165 | //********************************************************************** 166 | func compareCameras(cam1: Camera, cam2: Camera) -> Bool 167 | { 168 | let octets1 = cam1.address.split(separator: ".") 169 | let octets2 = cam2.address.split(separator: ".") 170 | let last1 = Int(octets1[3]) 171 | let last2 = Int(octets2[3]) 172 | return last1! < last2! 173 | } 174 | 175 | //********************************************************************** 176 | // setStatus 177 | //********************************************************************** 178 | func setStatus(_ last: Bool) 179 | { 180 | DispatchQueue.main.async 181 | { 182 | self.progressView.progress = Float(self.numDone) / 254.0 183 | self.statusLabel.text = String(format: "newCamerasFound".local, self.newCameras.count) 184 | if self.newCameras.count > 0 185 | { 186 | self.statusLabel.textColor = Utils.goodTextColor 187 | } 188 | else if last 189 | { 190 | self.statusLabel.textColor = Utils.badTextColor 191 | } 192 | if last 193 | { 194 | self.cancelButton.setTitle("done".local, for: UIControlState.normal) 195 | if self.newCameras.count > 0 && self.scanning 196 | { 197 | self.addCameras() 198 | DispatchQueue.main.asyncAfter(deadline: .now() + self.DISMISS_TIMEOUT, execute: 199 | { 200 | self.performSegue(withIdentifier: "UpdateCameras", sender: self) 201 | }) 202 | } 203 | } 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /RPiCameraViewer/ViewControllers/SettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2016-2019 Shawn Baker using the MIT License. 2 | import UIKit 3 | 4 | class SettingsViewController: InputViewController 5 | { 6 | // outlets 7 | @IBOutlet weak var mainScrollViewBottomConstraint: NSLayoutConstraint! 8 | @IBOutlet weak var mainScrollView: UIScrollView! 9 | @IBOutlet weak var cameraNameTextField: UITextField! 10 | @IBOutlet weak var allNetworksSwitch: UISwitch! 11 | @IBOutlet weak var timeoutIntField: IntTextField! 12 | @IBOutlet weak var portIntField: IntTextField! 13 | 14 | //********************************************************************** 15 | // viewDidLoad 16 | //********************************************************************** 17 | override func viewDidLoad() 18 | { 19 | super.viewDidLoad() 20 | 21 | initScrollView(mainScrollView, mainScrollViewBottomConstraint) 22 | 23 | cameraNameTextField.text = app.settings.cameraName 24 | allNetworksSwitch.isOn = app.settings.showAllCameras 25 | timeoutIntField.value = app.settings.scanTimeout 26 | portIntField.value = app.settings.port 27 | } 28 | 29 | //********************************************************************** 30 | // textFieldShouldReturn 31 | //********************************************************************** 32 | func textFieldShouldReturn(_ textField: UITextField) -> Bool 33 | { 34 | if textField == cameraNameTextField 35 | { 36 | textField.resignFirstResponder() 37 | } 38 | return true 39 | } 40 | 41 | //********************************************************************** 42 | // save 43 | //********************************************************************** 44 | func save() -> Bool 45 | { 46 | // error check the input values 47 | guard let name = cameraNameTextField.text, name.length > 0 else 48 | { 49 | Utils.error(self, "errorNoName") 50 | return false 51 | } 52 | guard let timeout = Utils.getIntTextField(self, timeoutIntField, "scanTimeout"), 53 | let port = Utils.getIntTextField(self, portIntField, "port") 54 | else 55 | { 56 | return false 57 | } 58 | 59 | // assign the new values to the settings 60 | app.settings.cameraName = name 61 | app.settings.showAllCameras = allNetworksSwitch.isOn 62 | app.settings.scanTimeout = timeout 63 | app.settings.port = port 64 | 65 | // save the settings 66 | app.save() 67 | return true 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /RPiCameraViewer/ViewControllers/TabBarController.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2016-2019 Shawn Baker using the MIT License. 2 | import UIKit 3 | 4 | class TabBarController: UITabBarController, UITabBarControllerDelegate 5 | { 6 | var camerasViewController: CamerasViewController? 7 | 8 | //********************************************************************** 9 | // viewDidLoad 10 | //********************************************************************** 11 | override func viewDidLoad() 12 | { 13 | super.viewDidLoad() 14 | delegate = self 15 | 16 | // get the view controllers we want to easily access 17 | for viewController in viewControllers! 18 | { 19 | // set the tab bar item icon colors 20 | var vc = viewController 21 | if let image = vc.tabBarItem.image 22 | { 23 | vc.tabBarItem.image = image.withRenderingMode(UIImageRenderingMode.alwaysOriginal) 24 | } 25 | 26 | // get the top view controller if necessary 27 | if let nav = vc as? UINavigationController 28 | { 29 | vc = nav.topViewController! 30 | } 31 | 32 | // get the cameras view controller 33 | if let cameras = vc as? CamerasViewController 34 | { 35 | camerasViewController = cameras 36 | } 37 | } 38 | } 39 | 40 | //********************************************************************** 41 | // tabBarController shouldSelect 42 | //********************************************************************** 43 | func tabBarController(_ tabBarController: UITabBarController, 44 | shouldSelect viewController: UIViewController) -> Bool 45 | { 46 | if let navController = selectedViewController as? UINavigationController 47 | { 48 | if let settingsController = navController.topViewController as? SettingsViewController 49 | { 50 | let result = settingsController.save() 51 | camerasViewController?.refreshCameras() 52 | return result 53 | } 54 | } 55 | return true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /RPiCameraViewer/ViewControllers/VideoViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2016-2019 Shawn Baker using the MIT License. 2 | import UIKit 3 | import AVFoundation 4 | import VideoToolbox 5 | import Photos 6 | 7 | class VideoViewController: UIViewController 8 | { 9 | // constants 10 | let READ_SIZE = 16384 11 | let MAX_READ_ERRORS = 300 12 | let FADE_OUT_WAIT_TIME = 8.0 13 | let FADE_OUT_INTERVAL = 1.0 14 | let FADE_IN_INTERVAL = 0.1 15 | 16 | // outlets 17 | @IBOutlet weak var imageView: UIImageView! 18 | @IBOutlet weak var statusLabel: UILabel! 19 | @IBOutlet weak var nameLabel: UILabel! 20 | @IBOutlet weak var closeButton: UIButton! 21 | @IBOutlet weak var snapshotButton: UIButton! 22 | 23 | // instance variables 24 | var camera: Camera? 25 | var fadeOutTimer: Timer? 26 | var running = false 27 | var close = false 28 | var dispatchGroup = DispatchGroup() 29 | var zoomPan: ZoomPan? 30 | let app = UIApplication.shared.delegate as! AppDelegate 31 | var formatDescription: CMVideoFormatDescription? 32 | var videoSession: VTDecompressionSession? 33 | var fullsps: [UInt8]? 34 | var fullpps: [UInt8]? 35 | var sps: [UInt8]? 36 | var pps: [UInt8]? 37 | 38 | //********************************************************************** 39 | // viewDidLoad 40 | //********************************************************************** 41 | override func viewDidLoad() 42 | { 43 | // initialize the view and controls 44 | super.viewDidLoad() 45 | nameLabel.text = camera!.name 46 | zoomPan = ZoomPan(imageView) 47 | 48 | // set up the tap and double tap gesture recognizers 49 | let doubleTap = UITapGestureRecognizer(target: self, action: #selector(self.handleDoubleTapGesture(_:))) 50 | doubleTap.numberOfTapsRequired = 2 51 | view.addGestureRecognizer(doubleTap) 52 | let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleTapGesture(_:))) 53 | tap.numberOfTapsRequired = 1 54 | tap.require(toFail: doubleTap) 55 | view.addGestureRecognizer(tap) 56 | 57 | // set up the pinch and pan gesture recognizers 58 | let pinch = UIPinchGestureRecognizer(target: zoomPan, action: #selector(zoomPan!.handlePinchGesture(_:))) 59 | view.addGestureRecognizer(pinch) 60 | let pan = UIPanGestureRecognizer(target: zoomPan, action: #selector(zoomPan!.handlePanGesture(_:))) 61 | pan.maximumNumberOfTouches = 2 62 | view.addGestureRecognizer(pan) 63 | 64 | // don't let the device dim or sleep while showing video 65 | UIApplication.shared.isIdleTimerDisabled = true 66 | 67 | // start reading the stream and passing the data to the video layer 68 | app.videoViewController = self 69 | start() 70 | } 71 | 72 | //********************************************************************** 73 | // start 74 | //********************************************************************** 75 | func start() 76 | { 77 | // set the status label 78 | statusLabel.text = "initializingVideo".local 79 | statusLabel.textColor = Utils.goodTextColor 80 | statusLabel.isHidden = false 81 | imageView.image = nil 82 | 83 | // start reading the stream and decoding the video 84 | if createReadThread() 85 | { 86 | // fade out after a while 87 | startFadeOutTimer() 88 | } 89 | 90 | // start listening for orientation changes 91 | NotificationCenter.default.addObserver(self, selector: #selector(orientationDidChange), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil) 92 | } 93 | 94 | //********************************************************************** 95 | // stop 96 | //********************************************************************** 97 | func stop() 98 | { 99 | running = false 100 | } 101 | 102 | //********************************************************************** 103 | // stopVideo 104 | //********************************************************************** 105 | func stopVideo() 106 | { 107 | // stop listening for orientation changes 108 | NotificationCenter.default.removeObserver(self) 109 | 110 | // stop fading out the controls 111 | stopFadeOutTimer() 112 | 113 | // terminate the video processing 114 | destroyVideoSession() 115 | 116 | // set the status label 117 | statusError("videoStopped".local) 118 | 119 | // close the controller if necessary 120 | if close 121 | { 122 | dismiss(animated: true) 123 | } 124 | } 125 | 126 | //********************************************************************** 127 | // statusError 128 | //********************************************************************** 129 | func statusError(_ message: String) 130 | { 131 | statusLabel.text = message 132 | statusLabel.textColor = Utils.badTextColor 133 | statusLabel.isHidden = false 134 | snapshotButton.isHidden = true 135 | stopFadeOutTimer() 136 | } 137 | 138 | //********************************************************************** 139 | // orientationDidChange 140 | //********************************************************************** 141 | @objc func orientationDidChange() 142 | { 143 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: 144 | { 145 | self.zoomPan?.reset() 146 | }) 147 | } 148 | 149 | //********************************************************************** 150 | // viewWillDisappear 151 | //********************************************************************** 152 | override func viewWillDisappear(_ animated: Bool) 153 | { 154 | UIApplication.shared.isIdleTimerDisabled = false 155 | app.videoViewController = nil 156 | } 157 | 158 | //********************************************************************** 159 | // handleCloseButtonTouchUpInside 160 | //********************************************************************** 161 | @IBAction func handleCloseButtonTouchUpInside(_ sender: Any) 162 | { 163 | if running 164 | { 165 | close = true 166 | stop() 167 | } 168 | else 169 | { 170 | dismiss(animated: true) 171 | } 172 | } 173 | 174 | //********************************************************************** 175 | // handleSnapshotButtonTouchUpInside 176 | //********************************************************************** 177 | @IBAction func handleSnapshotButtonTouchUpInside(_ sender: Any) 178 | { 179 | startFadeOutTimer() 180 | 181 | // check permissions 182 | let status = PHPhotoLibrary.authorizationStatus() 183 | if status == .authorized 184 | { 185 | takeSnapshot() 186 | } 187 | else if status == .notDetermined 188 | { 189 | PHPhotoLibrary.requestAuthorization() 190 | { status in 191 | if status == .authorized 192 | { 193 | DispatchQueue.main.async 194 | { 195 | self.takeSnapshot() 196 | } 197 | } 198 | } 199 | } 200 | else 201 | { 202 | Utils.error(self, "errorNotAuthorizedToSavePhotos") 203 | } 204 | } 205 | 206 | //********************************************************************** 207 | // takeSnapshot 208 | //********************************************************************** 209 | func takeSnapshot() 210 | { 211 | // hide the controls 212 | self.nameLabel.isHidden = true 213 | self.closeButton.isHidden = true 214 | self.snapshotButton.isHidden = true 215 | 216 | // take the snapshot 217 | if let uiImage = Utils.getSnapshot(view) 218 | { 219 | UIImageWriteToSavedPhotosAlbum(uiImage, self, #selector(imageSaved(_:didFinishSavingWithError:contextInfo:)), nil) 220 | } 221 | 222 | // show the controls 223 | self.nameLabel.isHidden = false 224 | self.closeButton.isHidden = false 225 | self.snapshotButton.isHidden = false 226 | } 227 | 228 | //********************************************************************** 229 | // handleTapGesture 230 | //********************************************************************** 231 | @objc func handleTapGesture(_ tap: UITapGestureRecognizer) 232 | { 233 | fadeIn() 234 | } 235 | 236 | //********************************************************************** 237 | // handleDoubleTapGesture 238 | //********************************************************************** 239 | @objc func handleDoubleTapGesture(_ tap: UITapGestureRecognizer) 240 | { 241 | zoomPan?.setZoomPan(1, CGPoint.zero) 242 | } 243 | 244 | //********************************************************************** 245 | // startFadeOutTimer 246 | //********************************************************************** 247 | func startFadeOutTimer() 248 | { 249 | stopFadeOutTimer() 250 | fadeOutTimer = Timer.scheduledTimer(timeInterval: FADE_OUT_WAIT_TIME, target: self, selector: #selector(fadeOut), userInfo: nil, repeats: false) 251 | } 252 | 253 | //********************************************************************** 254 | // stopFadeOutTimer 255 | //********************************************************************** 256 | func stopFadeOutTimer() 257 | { 258 | if let timer = fadeOutTimer, timer.isValid 259 | { 260 | timer.invalidate() 261 | } 262 | fadeOutTimer = nil 263 | } 264 | 265 | //********************************************************************** 266 | // fadeIn 267 | //********************************************************************** 268 | func fadeIn() 269 | { 270 | stopFadeOutTimer() 271 | UIView.animate(withDuration: FADE_IN_INTERVAL, delay: 0, options: UIViewAnimationOptions.curveEaseIn, animations: 272 | { 273 | self.statusLabel.alpha = 1.0 274 | self.nameLabel.alpha = 1.0 275 | self.closeButton.alpha = 1.0 276 | self.snapshotButton.alpha = 1.0 277 | }, 278 | completion: { (Bool) -> Void in self.startFadeOutTimer() }) 279 | } 280 | 281 | //********************************************************************** 282 | // fadeOut 283 | //********************************************************************** 284 | @objc func fadeOut() 285 | { 286 | stopFadeOutTimer() 287 | UIView.animate(withDuration: FADE_OUT_INTERVAL, delay: 0.0, options: UIViewAnimationOptions.curveEaseIn, animations: 288 | { 289 | self.statusLabel.alpha = 0.0 290 | self.nameLabel.alpha = 0.0 291 | self.closeButton.alpha = 0.0 292 | self.snapshotButton.alpha = 0.0 293 | }, 294 | completion: nil) 295 | } 296 | 297 | //********************************************************************** 298 | // createReadThread 299 | //********************************************************************** 300 | func createReadThread() -> Bool 301 | { 302 | if camera == nil 303 | { 304 | statusError("errorNoCamera".local) 305 | return false 306 | } 307 | 308 | var address = camera!.address 309 | if !Utils.isIpAddress(address) 310 | { 311 | address = Utils.resolveHostname(address) 312 | if address.isEmpty 313 | { 314 | let message = String(format: "errorCouldntResolveHostname".local, camera!.address) 315 | statusError(message) 316 | return false 317 | } 318 | } 319 | 320 | DispatchQueue.global(qos: .background).async 321 | { 322 | self.dispatchGroup.enter() 323 | self.dispatchGroup.notify(queue: .main) 324 | { 325 | self.stopVideo() 326 | } 327 | let socket = openSocket(address, Int32(self.camera!.port), Int32(self.app.settings.scanTimeout)) 328 | if (socket >= 0) 329 | { 330 | var numZeroes = 0 331 | var numReadErrors = 0 332 | var nal = [UInt8]() 333 | let ptr = UnsafeMutablePointer.allocate(capacity: self.READ_SIZE) 334 | let buffer = UnsafeMutableBufferPointer.init(start: ptr, count: self.READ_SIZE) 335 | var gotHeader = false 336 | self.running = true 337 | while self.running && numReadErrors < self.MAX_READ_ERRORS 338 | { 339 | let len = readSocket(socket, ptr, Int32(self.READ_SIZE)) 340 | if len > 0 341 | { 342 | numReadErrors = 0 343 | for i in 0..= 3 354 | { 355 | while numZeroes > 3 356 | { 357 | nal.append(0) 358 | numZeroes -= 1 359 | } 360 | if gotHeader 361 | { 362 | if !self.running { break } 363 | self.processNal(&nal) 364 | } 365 | nal = [0, 0, 0, 1] 366 | gotHeader = true 367 | } 368 | else 369 | { 370 | while numZeroes > 0 371 | { 372 | nal.append(0) 373 | numZeroes -= 1 374 | } 375 | nal.append(b) 376 | } 377 | numZeroes = 0 378 | } 379 | } 380 | } 381 | else 382 | { 383 | numReadErrors += 1 384 | } 385 | } 386 | closeSocket(socket) 387 | ptr.deallocate() 388 | } 389 | self.dispatchGroup.leave() 390 | } 391 | 392 | return true 393 | } 394 | 395 | //********************************************************************** 396 | // processNal 397 | //********************************************************************** 398 | func processNal(_ nal: inout [UInt8]) 399 | { 400 | // replace the start code with the NAL size 401 | let len = nal.count - 4 402 | var lenBig = CFSwapInt32HostToBig(UInt32(len)) 403 | memcpy(&nal, &lenBig, 4) 404 | 405 | // create the video session when we have the SPS and PPS records 406 | let nalType = nal[4] & 0x1F 407 | if nalType == 7 408 | { 409 | fullsps = nal 410 | } 411 | else if nalType == 8 412 | { 413 | fullpps = nal 414 | } 415 | if fullsps != nil && fullpps != nil 416 | { 417 | destroyVideoSession() 418 | sps = Array(fullsps![4...]) 419 | pps = Array(fullpps![4...]) 420 | _ = createVideoSession() 421 | fullsps = nil 422 | fullpps = nil 423 | DispatchQueue.main.async 424 | { 425 | self.statusLabel.isHidden = true 426 | } 427 | } 428 | 429 | // decode the video NALs 430 | if videoSession != nil && (nalType == 1 || nalType == 5) 431 | { 432 | _ = decodeNal(nal) 433 | } 434 | } 435 | 436 | //********************************************************************** 437 | // decodeNal 438 | //********************************************************************** 439 | private func decodeNal(_ nal: [UInt8]) -> Bool 440 | { 441 | // create the block buffer from the NAL data 442 | var blockBuffer: CMBlockBuffer? = nil 443 | let nalPointer = UnsafeMutablePointer(mutating: nal) 444 | var status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, nalPointer, nal.count, kCFAllocatorNull, nil, 0, nal.count, 0, &blockBuffer) 445 | if status != kCMBlockBufferNoErr 446 | { 447 | return false 448 | } 449 | 450 | // create the sample buffer from the block buffer 451 | var sampleBuffer: CMSampleBuffer? 452 | let sampleSizeArray = [nal.count] 453 | status = CMSampleBufferCreateReady(kCFAllocatorDefault, blockBuffer, formatDescription, 1, 0, nil, 1, sampleSizeArray, &sampleBuffer) 454 | if status != noErr 455 | { 456 | return false 457 | } 458 | if let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer!, true) 459 | { 460 | let dictionary = unsafeBitCast(CFArrayGetValueAtIndex(attachments, 0), to: CFMutableDictionary.self) 461 | CFDictionarySetValue(dictionary, Unmanaged.passUnretained(kCMSampleAttachmentKey_DisplayImmediately).toOpaque(), 462 | Unmanaged.passUnretained(kCFBooleanTrue).toOpaque()) 463 | } 464 | 465 | // pass the sample buffer to the decoder 466 | if let buffer = sampleBuffer, CMSampleBufferGetNumSamples(buffer) > 0 467 | { 468 | var infoFlags = VTDecodeInfoFlags(rawValue: 0) 469 | status = VTDecompressionSessionDecodeFrame(videoSession!, buffer, [._EnableAsynchronousDecompression], nil, &infoFlags) 470 | } 471 | return true 472 | } 473 | 474 | //********************************************************************** 475 | // createVideoSession 476 | //********************************************************************** 477 | private func createVideoSession() -> Bool 478 | { 479 | // create a new format description with the SPS and PPS records 480 | formatDescription = nil 481 | let parameters = [UnsafePointer(pps!), UnsafePointer(sps!)] 482 | let sizes = [pps!.count, sps!.count] 483 | var status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, UnsafePointer(parameters), sizes, 4, &formatDescription) 484 | if status != noErr 485 | { 486 | return false 487 | } 488 | let dimensions = CMVideoFormatDescriptionGetDimensions(formatDescription!) 489 | DispatchQueue.main.async 490 | { 491 | self.zoomPan?.setVideoSize(CGFloat(dimensions.width), CGFloat(dimensions.height)) 492 | } 493 | 494 | // create the decoder parameters 495 | let decoderParameters = NSMutableDictionary() 496 | let destinationPixelBufferAttributes = NSMutableDictionary() 497 | destinationPixelBufferAttributes.setValue(NSNumber(value: kCVPixelFormatType_32BGRA), forKey: kCVPixelBufferPixelFormatTypeKey as String) 498 | 499 | // create the callback for getting snapshots 500 | var callback = VTDecompressionOutputCallbackRecord() 501 | callback.decompressionOutputCallback = globalDecompressionCallback 502 | callback.decompressionOutputRefCon = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) 503 | 504 | // create the video session 505 | status = VTDecompressionSessionCreate(nil, formatDescription!, decoderParameters, destinationPixelBufferAttributes, &callback, &videoSession) 506 | return status == noErr 507 | } 508 | 509 | //********************************************************************** 510 | // destroyVideoSession 511 | //********************************************************************** 512 | func destroyVideoSession() 513 | { 514 | if let session = videoSession 515 | { 516 | VTDecompressionSessionWaitForAsynchronousFrames(session) 517 | VTDecompressionSessionInvalidate(session) 518 | videoSession = nil 519 | } 520 | sps = nil 521 | pps = nil 522 | formatDescription = nil 523 | } 524 | 525 | //********************************************************************** 526 | // decompressionCallback 527 | //********************************************************************** 528 | func decompressionCallback(_ sourceFrameRefCon: UnsafeMutableRawPointer?, _ status: OSStatus, _ infoFlags: VTDecodeInfoFlags, _ imageBuffer: CVImageBuffer?, _ presentationTimeStamp: CMTime, _ presentationDuration: CMTime) 529 | { 530 | if running, let cvImageBuffer = imageBuffer 531 | { 532 | let ciImage = CIImage(cvImageBuffer: cvImageBuffer) 533 | let size = CVImageBufferGetEncodedSize(cvImageBuffer) 534 | let context = CIContext() 535 | if let cgImage = context.createCGImage(ciImage, from: CGRect(origin: CGPoint.zero, size: size)) 536 | { 537 | let uiImage = UIImage(cgImage: cgImage) 538 | DispatchQueue.main.async 539 | { 540 | self.imageView.image = uiImage 541 | } 542 | } 543 | } 544 | } 545 | 546 | //********************************************************************** 547 | // imageSaved 548 | //********************************************************************** 549 | @objc func imageSaved(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) 550 | { 551 | if let error = error 552 | { 553 | let ac = UIAlertController(title: "Save Error", message: error.localizedDescription, preferredStyle: .alert) 554 | ac.addAction(UIAlertAction(title: "OK", style: .default)) 555 | present(ac, animated: true) 556 | } 557 | else 558 | { 559 | AudioServicesPlaySystemSoundWithCompletion(SystemSoundID(1108), nil) 560 | } 561 | } 562 | } 563 | 564 | //********************************************************************** 565 | // globalDecompressionCallback 566 | //********************************************************************** 567 | private func globalDecompressionCallback(_ decompressionOutputRefCon: UnsafeMutableRawPointer?, _ sourceFrameRefCon: UnsafeMutableRawPointer?, _ status: OSStatus, _ infoFlags: VTDecodeInfoFlags, _ imageBuffer: CVImageBuffer?, _ presentationTimeStamp: CMTime, _ presentationDuration: CMTime) -> Void 568 | { 569 | let videoController: VideoViewController = unsafeBitCast(decompressionOutputRefCon, to: VideoViewController.self) 570 | videoController.decompressionCallback(sourceFrameRefCon, status, infoFlags, imageBuffer, presentationTimeStamp, presentationDuration) 571 | } 572 | -------------------------------------------------------------------------------- /RPiCameraViewer/Views/Button.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Shawn Baker using the MIT License. 2 | import UIKit 3 | 4 | @IBDesignable class Button: UIButton 5 | { 6 | //********************************************************************** 7 | // init 8 | //********************************************************************** 9 | required init?(coder aDecoder: NSCoder) 10 | { 11 | super.init(coder: aDecoder) 12 | self.tintColor = UIColor.white 13 | self.backgroundColor = Utils.primaryColor 14 | } 15 | 16 | //********************************************************************** 17 | // cornerRadius 18 | //********************************************************************** 19 | @IBInspectable var cornerRadius: CGFloat = 0 20 | { 21 | didSet 22 | { 23 | self.layer.cornerRadius = cornerRadius 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /RPiCameraViewer/Views/IntTextField.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2016-2017 Shawn Baker using the MIT License. 2 | import Foundation 3 | import UIKit 4 | 5 | @IBDesignable 6 | class IntTextField: UITextField, UITextFieldDelegate 7 | { 8 | // instance variables 9 | @IBInspectable public var maxLen: Int = 10 10 | @IBInspectable public var min: Int = Int.min 11 | @IBInspectable public var max: Int = Int.max 12 | 13 | //********************************************************************** 14 | // init 15 | //********************************************************************** 16 | required init?(coder decoder: NSCoder) 17 | { 18 | super.init(coder: decoder) 19 | initialize() 20 | } 21 | 22 | //********************************************************************** 23 | // init 24 | //********************************************************************** 25 | override init(frame: CGRect) 26 | { 27 | super.init(frame: frame) 28 | initialize() 29 | } 30 | 31 | //********************************************************************** 32 | // initialize 33 | //********************************************************************** 34 | func initialize() 35 | { 36 | delegate = self 37 | let toolbar: UIToolbar = UIToolbar() 38 | toolbar.items = 39 | [ 40 | UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.flexibleSpace, target: self, action: nil), 41 | UIBarButtonItem(title: "done".local, style: UIBarButtonItemStyle.done, target: self, action: #selector(UITextField.resignFirstResponder)) 42 | ] 43 | toolbar.sizeToFit() 44 | inputAccessoryView = toolbar 45 | } 46 | 47 | //********************************************************************** 48 | // textField shouldChangeCharactersIn 49 | //********************************************************************** 50 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool 51 | { 52 | if string.count == 0 53 | { 54 | return true 55 | } 56 | 57 | let text = textField.text ?? "" 58 | let newText = (text as NSString).replacingCharacters(in: range, with: string) 59 | 60 | let pattern = "^" + ((min < 0) ? "(\\+|-)" : "") + "\\d+$" 61 | let regex = try! NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) 62 | if regex.firstMatch(in: newText, options: [], range: NSRange(location: 0, length: newText.count)) == nil 63 | { 64 | return false 65 | } 66 | 67 | return newText.count <= maxLen 68 | } 69 | 70 | //********************************************************************** 71 | // value 72 | //********************************************************************** 73 | var value: Int? 74 | { 75 | get 76 | { 77 | let s = text ?? "" 78 | if s.count > 0, 79 | let n = Int(s) 80 | //n >= min && n <= max 81 | { 82 | return n 83 | } 84 | return nil 85 | } 86 | set 87 | { 88 | if let n = newValue 89 | { 90 | text = String(n) 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /RPiCameraViewer/Views/Popup.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Shawn Baker using the MIT License. 2 | import UIKit 3 | 4 | @IBDesignable class Popup: UIView 5 | { 6 | //********************************************************************** 7 | // cornerRadius 8 | //********************************************************************** 9 | @IBInspectable var cornerRadius: CGFloat = 0 10 | { 11 | didSet 12 | { 13 | self.layer.cornerRadius = cornerRadius 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /RPiCameraViewer/en.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UILabel"; text = "Height"; ObjectID = "05V-Om-nG0"; */ 3 | "05V-Om-nG0.text" = "Height"; 4 | 5 | /* Class = "UINavigationItem"; title = "Camera Details"; ObjectID = "0eF-V6-LhQ"; */ 6 | "0eF-V6-LhQ.title" = "Camera Details"; 7 | 8 | /* Class = "UILabel"; text = "Width"; ObjectID = "4X6-um-Mab"; */ 9 | "4X6-um-Mab.text" = "Width"; 10 | 11 | /* Class = "UILabel"; text = "Name"; ObjectID = "CVH-WL-J4n"; */ 12 | "CVH-WL-J4n.text" = "Name"; 13 | 14 | /* Class = "UILabel"; text = "Frames per second"; ObjectID = "Ccg-5J-P4z"; */ 15 | "Ccg-5J-P4z.text" = "Frames per second"; 16 | 17 | /* Class = "UILabel"; text = "Detail"; ObjectID = "GcS-y3-xXJ"; */ 18 | "GcS-y3-xXJ.text" = "Detail"; 19 | 20 | /* Class = "UILabel"; text = "Label"; ObjectID = "HlT-gq-Fpv"; */ 21 | "HlT-gq-Fpv.text" = "Label"; 22 | 23 | /* Class = "UILabel"; text = "Address"; ObjectID = "JEQ-hB-zjV"; */ 24 | "JEQ-hB-zjV.text" = "Address"; 25 | 26 | /* Class = "UILabel"; text = "Bits per second"; ObjectID = "KI7-Vt-YTf"; */ 27 | "KI7-Vt-YTf.text" = "Bits per second"; 28 | 29 | /* Class = "UINavigationItem"; title = "RPi Camera Viewer"; ObjectID = "Vhv-ch-keu"; */ 30 | "Vhv-ch-keu.title" = "RPi Camera Viewer"; 31 | 32 | /* Class = "UILabel"; text = "Network"; ObjectID = "YhM-O0-Jna"; */ 33 | "YhM-O0-Jna.text" = "Network"; 34 | 35 | /* Class = "UILabel"; text = "Title"; ObjectID = "aOY-O9-6Gw"; */ 36 | "aOY-O9-6Gw.text" = "Title"; 37 | 38 | /* Class = "UILabel"; text = "Port"; ObjectID = "lQO-VA-9tZ"; */ 39 | "lQO-VA-9tZ.text" = "Port"; 40 | 41 | /* Class = "UIBarButtonItem"; title = "Cancel"; ObjectID = "n6X-88-05y"; */ 42 | "n6X-88-05y.title" = "Cancel"; 43 | 44 | /* Class = "UIBarButtonItem"; title = "Cameras"; ObjectID = "oTl-0M-ny2"; */ 45 | "oTl-0M-ny2.title" = "Cameras"; 46 | 47 | /* Class = "UIBarButtonItem"; title = "Save"; ObjectID = "qAp-C8-WeZ"; */ 48 | "qAp-C8-WeZ.title" = "Save"; 49 | --------------------------------------------------------------------------------