├── .gitignore ├── LICENSE ├── Podfile ├── Podfile.lock ├── README.md ├── RTSPViewer.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── RTSPViewer.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist └── RTSPViewer ├── AppDelegate.swift ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── Base.lproj └── LaunchScreen.storyboard ├── Cells ├── TextInputCell.swift └── VideoCell.swift ├── Controllers ├── AddStreamViewController.swift ├── RTSPStreamViewController.swift └── VideoStreamController.swift ├── Extensions ├── Array+EXT.swift ├── String+EXT.swift ├── UICollectionView+EXT.swift └── UITextField+EXT.swift ├── Info.plist ├── Protocols ├── Evaluatable.swift └── Validatable.swift ├── RTSPViewer-Bridging-Header.h ├── SceneDelegate.swift ├── Storyboards ├── AddStream.storyboard ├── Base.lproj │ └── Main.storyboard └── VideoStream.storyboard ├── Validator.swift ├── ViewController.swift └── Views ├── SSCheckMarkView.swift └── VideoView.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | # https://github.com/github/gitignore/blob/master/Swift.gitignore 5 | 6 | ## Build generated 7 | build/ 8 | DerivedData/ 9 | 10 | ## Various settings 11 | .DS_Store 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | ## Other 23 | *.moved-aside 24 | *.xccheckout 25 | *.xcscmblueprint 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | .build/ 44 | 45 | # CocoaPods 46 | # 47 | # We recommend against adding the Pods directory to your .gitignore. However 48 | # you should judge for yourself, the pros and cons are mentioned at: 49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 50 | # 51 | Pods/ 52 | 53 | # Carthage 54 | # 55 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 56 | # Carthage/Checkouts 57 | 58 | Carthage/Build 59 | 60 | # fastlane 61 | # 62 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 63 | # screenshots whenever they are needed. 64 | # For more information about the recommended setup visit: 65 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 66 | 67 | fastlane/report.xml 68 | fastlane/Preview.html 69 | fastlane/screenshots 70 | fastlane/test_output 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '9.0' 3 | 4 | target 'RTSPViewer' do 5 | use_frameworks! 6 | 7 | # Pods for RTSPViewer 8 | pod 'MobileVLCKit' 9 | end 10 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - MobileVLCKit (3.3.9) 3 | 4 | DEPENDENCIES: 5 | - MobileVLCKit 6 | 7 | SPEC REPOS: 8 | trunk: 9 | - MobileVLCKit 10 | 11 | SPEC CHECKSUMS: 12 | MobileVLCKit: cf10c4a138d89f3843b6290f94823609b34215a1 13 | 14 | PODFILE CHECKSUM: ba80bddaecb0d0a204de21a972373d535fee6393 15 | 16 | COCOAPODS: 1.8.4 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RTSPViewer 2 | View RTSP streams 3 | 4 | # Run Project 5 | To run the project: 6 | 7 | * Clone this repository. 8 | * pod install 9 | * Open RTSPViewer.xcworkspace 10 | * Build and run in Xcode 11 | 12 | # License 13 | This project is licensed under the MIT License. 14 | -------------------------------------------------------------------------------- /RTSPViewer.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3C14F94423B5EFEA00A41B82 /* RTSPStreamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14F94323B5EFEA00A41B82 /* RTSPStreamViewController.swift */; }; 11 | 3C18FC9623C1D44200E533F8 /* TextInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C18FC9523C1D44200E533F8 /* TextInputCell.swift */; }; 12 | 3C786C7D23BDA7AB009B7351 /* AddStreamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C786C7C23BDA7AB009B7351 /* AddStreamViewController.swift */; }; 13 | 3C786C7F23BDA842009B7351 /* AddStream.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3C786C7E23BDA842009B7351 /* AddStream.storyboard */; }; 14 | 3C7C154223C29EE900F998E0 /* UITextField+EXT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C7C154123C29EE900F998E0 /* UITextField+EXT.swift */; }; 15 | 3C7C154523C2A30B00F998E0 /* Validator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C7C154423C2A30B00F998E0 /* Validator.swift */; }; 16 | 3C7C154723C2A34C00F998E0 /* Validatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C7C154623C2A34C00F998E0 /* Validatable.swift */; }; 17 | 3C7C154A23C2A3D900F998E0 /* Evaluatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C7C154923C2A3D900F998E0 /* Evaluatable.swift */; }; 18 | 3C7C154C23C2E35D00F998E0 /* String+EXT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C7C154B23C2E35D00F998E0 /* String+EXT.swift */; }; 19 | 3C7C154E23C5A38600F998E0 /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C7C154D23C5A38600F998E0 /* VideoView.swift */; }; 20 | 3C7C155223C5AF2600F998E0 /* VideoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C7C155123C5AF2600F998E0 /* VideoCell.swift */; }; 21 | 3CFB58F323DD4DA200897303 /* Array+EXT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFB58F223DD4DA200897303 /* Array+EXT.swift */; }; 22 | 3CFB58F523DD4E2000897303 /* UICollectionView+EXT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFB58F423DD4E2000897303 /* UICollectionView+EXT.swift */; }; 23 | 3CFB58F723DE541700897303 /* SSCheckMarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFB58F623DE541700897303 /* SSCheckMarkView.swift */; }; 24 | 3CFB58F923DE968A00897303 /* VideoStreamController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFB58F823DE968A00897303 /* VideoStreamController.swift */; }; 25 | 3CFB58FB23DE96D000897303 /* VideoStream.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3CFB58FA23DE96D000897303 /* VideoStream.storyboard */; }; 26 | 6BD5857F6652C392FBBB360B /* Pods_RTSPViewer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AFB07B83F8453FC533EAAB56 /* Pods_RTSPViewer.framework */; }; 27 | 84252F7723B56C9A0016D338 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84252F7623B56C9A0016D338 /* AppDelegate.swift */; }; 28 | 84252F7923B56C9A0016D338 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84252F7823B56C9A0016D338 /* SceneDelegate.swift */; }; 29 | 84252F7E23B56C9A0016D338 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84252F7C23B56C9A0016D338 /* Main.storyboard */; }; 30 | 84252F8023B56C9E0016D338 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84252F7F23B56C9E0016D338 /* Assets.xcassets */; }; 31 | 84252F8323B56C9E0016D338 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84252F8123B56C9E0016D338 /* LaunchScreen.storyboard */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXFileReference section */ 35 | 3C14F94323B5EFEA00A41B82 /* RTSPStreamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTSPStreamViewController.swift; sourceTree = ""; }; 36 | 3C18FC9523C1D44200E533F8 /* TextInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputCell.swift; sourceTree = ""; }; 37 | 3C786C7C23BDA7AB009B7351 /* AddStreamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddStreamViewController.swift; sourceTree = ""; }; 38 | 3C786C7E23BDA842009B7351 /* AddStream.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = AddStream.storyboard; sourceTree = ""; }; 39 | 3C7C154123C29EE900F998E0 /* UITextField+EXT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+EXT.swift"; sourceTree = ""; }; 40 | 3C7C154423C2A30B00F998E0 /* Validator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validator.swift; sourceTree = ""; }; 41 | 3C7C154623C2A34C00F998E0 /* Validatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validatable.swift; sourceTree = ""; }; 42 | 3C7C154923C2A3D900F998E0 /* Evaluatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Evaluatable.swift; sourceTree = ""; }; 43 | 3C7C154B23C2E35D00F998E0 /* String+EXT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+EXT.swift"; sourceTree = ""; }; 44 | 3C7C154D23C5A38600F998E0 /* VideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoView.swift; sourceTree = ""; }; 45 | 3C7C155023C5A48B00F998E0 /* RTSPViewer-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RTSPViewer-Bridging-Header.h"; sourceTree = ""; }; 46 | 3C7C155123C5AF2600F998E0 /* VideoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCell.swift; sourceTree = ""; }; 47 | 3CFB58F223DD4DA200897303 /* Array+EXT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+EXT.swift"; sourceTree = ""; }; 48 | 3CFB58F423DD4E2000897303 /* UICollectionView+EXT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionView+EXT.swift"; sourceTree = ""; }; 49 | 3CFB58F623DE541700897303 /* SSCheckMarkView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSCheckMarkView.swift; sourceTree = ""; }; 50 | 3CFB58F823DE968A00897303 /* VideoStreamController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoStreamController.swift; sourceTree = ""; }; 51 | 3CFB58FA23DE96D000897303 /* VideoStream.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = VideoStream.storyboard; sourceTree = ""; }; 52 | 84252F7323B56C990016D338 /* RTSPViewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RTSPViewer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 84252F7623B56C9A0016D338 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 54 | 84252F7823B56C9A0016D338 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 55 | 84252F7D23B56C9A0016D338 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 56 | 84252F7F23B56C9E0016D338 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 57 | 84252F8223B56C9E0016D338 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 58 | 84252F8423B56C9E0016D338 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 59 | AFB07B83F8453FC533EAAB56 /* Pods_RTSPViewer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RTSPViewer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 60 | CCAD255B48F722FEB9F90FB3 /* Pods-RTSPViewer.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RTSPViewer.debug.xcconfig"; path = "Target Support Files/Pods-RTSPViewer/Pods-RTSPViewer.debug.xcconfig"; sourceTree = ""; }; 61 | D86C30A2D8F688A7B77ED438 /* Pods-RTSPViewer.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RTSPViewer.release.xcconfig"; path = "Target Support Files/Pods-RTSPViewer/Pods-RTSPViewer.release.xcconfig"; sourceTree = ""; }; 62 | /* End PBXFileReference section */ 63 | 64 | /* Begin PBXFrameworksBuildPhase section */ 65 | 84252F7023B56C990016D338 /* Frameworks */ = { 66 | isa = PBXFrameworksBuildPhase; 67 | buildActionMask = 2147483647; 68 | files = ( 69 | 6BD5857F6652C392FBBB360B /* Pods_RTSPViewer.framework in Frameworks */, 70 | ); 71 | runOnlyForDeploymentPostprocessing = 0; 72 | }; 73 | /* End PBXFrameworksBuildPhase section */ 74 | 75 | /* Begin PBXGroup section */ 76 | 068C7946F50C72379975E10F /* Pods */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | CCAD255B48F722FEB9F90FB3 /* Pods-RTSPViewer.debug.xcconfig */, 80 | D86C30A2D8F688A7B77ED438 /* Pods-RTSPViewer.release.xcconfig */, 81 | ); 82 | path = Pods; 83 | sourceTree = ""; 84 | }; 85 | 3C14F94523B5EFF800A41B82 /* Controllers */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | 3C14F94323B5EFEA00A41B82 /* RTSPStreamViewController.swift */, 89 | 3C786C7C23BDA7AB009B7351 /* AddStreamViewController.swift */, 90 | 3CFB58F823DE968A00897303 /* VideoStreamController.swift */, 91 | ); 92 | path = Controllers; 93 | sourceTree = ""; 94 | }; 95 | 3C18FC9723C1D44A00E533F8 /* Cells */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | 3C18FC9523C1D44200E533F8 /* TextInputCell.swift */, 99 | 3C7C155123C5AF2600F998E0 /* VideoCell.swift */, 100 | ); 101 | path = Cells; 102 | sourceTree = ""; 103 | }; 104 | 3C786C8023BDA849009B7351 /* Storyboards */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 84252F7C23B56C9A0016D338 /* Main.storyboard */, 108 | 3C786C7E23BDA842009B7351 /* AddStream.storyboard */, 109 | 3CFB58FA23DE96D000897303 /* VideoStream.storyboard */, 110 | ); 111 | path = Storyboards; 112 | sourceTree = ""; 113 | }; 114 | 3C7C154323C29F2700F998E0 /* Extensions */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 3C7C154123C29EE900F998E0 /* UITextField+EXT.swift */, 118 | 3C7C154B23C2E35D00F998E0 /* String+EXT.swift */, 119 | 3CFB58F223DD4DA200897303 /* Array+EXT.swift */, 120 | 3CFB58F423DD4E2000897303 /* UICollectionView+EXT.swift */, 121 | ); 122 | path = Extensions; 123 | sourceTree = ""; 124 | }; 125 | 3C7C154823C2A3A600F998E0 /* Protocols */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 3C7C154623C2A34C00F998E0 /* Validatable.swift */, 129 | 3C7C154923C2A3D900F998E0 /* Evaluatable.swift */, 130 | ); 131 | path = Protocols; 132 | sourceTree = ""; 133 | }; 134 | 3C7C154F23C5A39000F998E0 /* Views */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 3CFB58F623DE541700897303 /* SSCheckMarkView.swift */, 138 | 3C7C154D23C5A38600F998E0 /* VideoView.swift */, 139 | ); 140 | path = Views; 141 | sourceTree = ""; 142 | }; 143 | 84252F6A23B56C990016D338 = { 144 | isa = PBXGroup; 145 | children = ( 146 | 84252F7523B56C990016D338 /* RTSPViewer */, 147 | 84252F7423B56C990016D338 /* Products */, 148 | 068C7946F50C72379975E10F /* Pods */, 149 | DA1E2356BF40F540E08F3FE8 /* Frameworks */, 150 | ); 151 | sourceTree = ""; 152 | }; 153 | 84252F7423B56C990016D338 /* Products */ = { 154 | isa = PBXGroup; 155 | children = ( 156 | 84252F7323B56C990016D338 /* RTSPViewer.app */, 157 | ); 158 | name = Products; 159 | sourceTree = ""; 160 | }; 161 | 84252F7523B56C990016D338 /* RTSPViewer */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 84252F7623B56C9A0016D338 /* AppDelegate.swift */, 165 | 84252F7823B56C9A0016D338 /* SceneDelegate.swift */, 166 | 3C7C154423C2A30B00F998E0 /* Validator.swift */, 167 | 3C7C154F23C5A39000F998E0 /* Views */, 168 | 3C7C154823C2A3A600F998E0 /* Protocols */, 169 | 3C7C154323C29F2700F998E0 /* Extensions */, 170 | 3C18FC9723C1D44A00E533F8 /* Cells */, 171 | 3C14F94523B5EFF800A41B82 /* Controllers */, 172 | 3C786C8023BDA849009B7351 /* Storyboards */, 173 | 84252F7F23B56C9E0016D338 /* Assets.xcassets */, 174 | 84252F8123B56C9E0016D338 /* LaunchScreen.storyboard */, 175 | 84252F8423B56C9E0016D338 /* Info.plist */, 176 | 3C7C155023C5A48B00F998E0 /* RTSPViewer-Bridging-Header.h */, 177 | ); 178 | path = RTSPViewer; 179 | sourceTree = ""; 180 | }; 181 | DA1E2356BF40F540E08F3FE8 /* Frameworks */ = { 182 | isa = PBXGroup; 183 | children = ( 184 | AFB07B83F8453FC533EAAB56 /* Pods_RTSPViewer.framework */, 185 | ); 186 | name = Frameworks; 187 | sourceTree = ""; 188 | }; 189 | /* End PBXGroup section */ 190 | 191 | /* Begin PBXNativeTarget section */ 192 | 84252F7223B56C990016D338 /* RTSPViewer */ = { 193 | isa = PBXNativeTarget; 194 | buildConfigurationList = 84252F8723B56C9E0016D338 /* Build configuration list for PBXNativeTarget "RTSPViewer" */; 195 | buildPhases = ( 196 | 3B486AF8E4668B884C59DC32 /* [CP] Check Pods Manifest.lock */, 197 | 84252F6F23B56C990016D338 /* Sources */, 198 | 84252F7023B56C990016D338 /* Frameworks */, 199 | 84252F7123B56C990016D338 /* Resources */, 200 | ); 201 | buildRules = ( 202 | ); 203 | dependencies = ( 204 | ); 205 | name = RTSPViewer; 206 | productName = RTSPViewer; 207 | productReference = 84252F7323B56C990016D338 /* RTSPViewer.app */; 208 | productType = "com.apple.product-type.application"; 209 | }; 210 | /* End PBXNativeTarget section */ 211 | 212 | /* Begin PBXProject section */ 213 | 84252F6B23B56C990016D338 /* Project object */ = { 214 | isa = PBXProject; 215 | attributes = { 216 | LastSwiftUpdateCheck = 1120; 217 | LastUpgradeCheck = 1120; 218 | ORGANIZATIONNAME = home; 219 | TargetAttributes = { 220 | 84252F7223B56C990016D338 = { 221 | CreatedOnToolsVersion = 11.2; 222 | }; 223 | }; 224 | }; 225 | buildConfigurationList = 84252F6E23B56C990016D338 /* Build configuration list for PBXProject "RTSPViewer" */; 226 | compatibilityVersion = "Xcode 9.3"; 227 | developmentRegion = en; 228 | hasScannedForEncodings = 0; 229 | knownRegions = ( 230 | en, 231 | Base, 232 | ); 233 | mainGroup = 84252F6A23B56C990016D338; 234 | productRefGroup = 84252F7423B56C990016D338 /* Products */; 235 | projectDirPath = ""; 236 | projectRoot = ""; 237 | targets = ( 238 | 84252F7223B56C990016D338 /* RTSPViewer */, 239 | ); 240 | }; 241 | /* End PBXProject section */ 242 | 243 | /* Begin PBXResourcesBuildPhase section */ 244 | 84252F7123B56C990016D338 /* Resources */ = { 245 | isa = PBXResourcesBuildPhase; 246 | buildActionMask = 2147483647; 247 | files = ( 248 | 3C786C7F23BDA842009B7351 /* AddStream.storyboard in Resources */, 249 | 84252F8323B56C9E0016D338 /* LaunchScreen.storyboard in Resources */, 250 | 84252F8023B56C9E0016D338 /* Assets.xcassets in Resources */, 251 | 3CFB58FB23DE96D000897303 /* VideoStream.storyboard in Resources */, 252 | 84252F7E23B56C9A0016D338 /* Main.storyboard in Resources */, 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | /* End PBXResourcesBuildPhase section */ 257 | 258 | /* Begin PBXShellScriptBuildPhase section */ 259 | 3B486AF8E4668B884C59DC32 /* [CP] Check Pods Manifest.lock */ = { 260 | isa = PBXShellScriptBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | ); 264 | inputFileListPaths = ( 265 | ); 266 | inputPaths = ( 267 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 268 | "${PODS_ROOT}/Manifest.lock", 269 | ); 270 | name = "[CP] Check Pods Manifest.lock"; 271 | outputFileListPaths = ( 272 | ); 273 | outputPaths = ( 274 | "$(DERIVED_FILE_DIR)/Pods-RTSPViewer-checkManifestLockResult.txt", 275 | ); 276 | runOnlyForDeploymentPostprocessing = 0; 277 | shellPath = /bin/sh; 278 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 279 | showEnvVarsInLog = 0; 280 | }; 281 | /* End PBXShellScriptBuildPhase section */ 282 | 283 | /* Begin PBXSourcesBuildPhase section */ 284 | 84252F6F23B56C990016D338 /* Sources */ = { 285 | isa = PBXSourcesBuildPhase; 286 | buildActionMask = 2147483647; 287 | files = ( 288 | 3C786C7D23BDA7AB009B7351 /* AddStreamViewController.swift in Sources */, 289 | 3C7C154223C29EE900F998E0 /* UITextField+EXT.swift in Sources */, 290 | 84252F7723B56C9A0016D338 /* AppDelegate.swift in Sources */, 291 | 3C14F94423B5EFEA00A41B82 /* RTSPStreamViewController.swift in Sources */, 292 | 3C7C154C23C2E35D00F998E0 /* String+EXT.swift in Sources */, 293 | 3C7C155223C5AF2600F998E0 /* VideoCell.swift in Sources */, 294 | 3CFB58F723DE541700897303 /* SSCheckMarkView.swift in Sources */, 295 | 3C7C154A23C2A3D900F998E0 /* Evaluatable.swift in Sources */, 296 | 3C7C154523C2A30B00F998E0 /* Validator.swift in Sources */, 297 | 3CFB58F523DD4E2000897303 /* UICollectionView+EXT.swift in Sources */, 298 | 3C7C154723C2A34C00F998E0 /* Validatable.swift in Sources */, 299 | 3C18FC9623C1D44200E533F8 /* TextInputCell.swift in Sources */, 300 | 3C7C154E23C5A38600F998E0 /* VideoView.swift in Sources */, 301 | 3CFB58F923DE968A00897303 /* VideoStreamController.swift in Sources */, 302 | 84252F7923B56C9A0016D338 /* SceneDelegate.swift in Sources */, 303 | 3CFB58F323DD4DA200897303 /* Array+EXT.swift in Sources */, 304 | ); 305 | runOnlyForDeploymentPostprocessing = 0; 306 | }; 307 | /* End PBXSourcesBuildPhase section */ 308 | 309 | /* Begin PBXVariantGroup section */ 310 | 84252F7C23B56C9A0016D338 /* Main.storyboard */ = { 311 | isa = PBXVariantGroup; 312 | children = ( 313 | 84252F7D23B56C9A0016D338 /* Base */, 314 | ); 315 | name = Main.storyboard; 316 | sourceTree = ""; 317 | }; 318 | 84252F8123B56C9E0016D338 /* LaunchScreen.storyboard */ = { 319 | isa = PBXVariantGroup; 320 | children = ( 321 | 84252F8223B56C9E0016D338 /* Base */, 322 | ); 323 | name = LaunchScreen.storyboard; 324 | sourceTree = ""; 325 | }; 326 | /* End PBXVariantGroup section */ 327 | 328 | /* Begin XCBuildConfiguration section */ 329 | 84252F8523B56C9E0016D338 /* Debug */ = { 330 | isa = XCBuildConfiguration; 331 | buildSettings = { 332 | ALWAYS_SEARCH_USER_PATHS = NO; 333 | CLANG_ANALYZER_NONNULL = YES; 334 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 335 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 336 | CLANG_CXX_LIBRARY = "libc++"; 337 | CLANG_ENABLE_MODULES = YES; 338 | CLANG_ENABLE_OBJC_ARC = YES; 339 | CLANG_ENABLE_OBJC_WEAK = YES; 340 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 341 | CLANG_WARN_BOOL_CONVERSION = YES; 342 | CLANG_WARN_COMMA = YES; 343 | CLANG_WARN_CONSTANT_CONVERSION = YES; 344 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 345 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 346 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 347 | CLANG_WARN_EMPTY_BODY = YES; 348 | CLANG_WARN_ENUM_CONVERSION = YES; 349 | CLANG_WARN_INFINITE_RECURSION = YES; 350 | CLANG_WARN_INT_CONVERSION = YES; 351 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 352 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 353 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 354 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 355 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 356 | CLANG_WARN_STRICT_PROTOTYPES = YES; 357 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 358 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 359 | CLANG_WARN_UNREACHABLE_CODE = YES; 360 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 361 | COPY_PHASE_STRIP = NO; 362 | DEBUG_INFORMATION_FORMAT = dwarf; 363 | ENABLE_STRICT_OBJC_MSGSEND = YES; 364 | ENABLE_TESTABILITY = YES; 365 | GCC_C_LANGUAGE_STANDARD = gnu11; 366 | GCC_DYNAMIC_NO_PIC = NO; 367 | GCC_NO_COMMON_BLOCKS = YES; 368 | GCC_OPTIMIZATION_LEVEL = 0; 369 | GCC_PREPROCESSOR_DEFINITIONS = ( 370 | "DEBUG=1", 371 | "$(inherited)", 372 | ); 373 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 374 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 375 | GCC_WARN_UNDECLARED_SELECTOR = YES; 376 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 377 | GCC_WARN_UNUSED_FUNCTION = YES; 378 | GCC_WARN_UNUSED_VARIABLE = YES; 379 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 380 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 381 | MTL_FAST_MATH = YES; 382 | ONLY_ACTIVE_ARCH = YES; 383 | SDKROOT = iphoneos; 384 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 385 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 386 | }; 387 | name = Debug; 388 | }; 389 | 84252F8623B56C9E0016D338 /* Release */ = { 390 | isa = XCBuildConfiguration; 391 | buildSettings = { 392 | ALWAYS_SEARCH_USER_PATHS = NO; 393 | CLANG_ANALYZER_NONNULL = YES; 394 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 395 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 396 | CLANG_CXX_LIBRARY = "libc++"; 397 | CLANG_ENABLE_MODULES = YES; 398 | CLANG_ENABLE_OBJC_ARC = YES; 399 | CLANG_ENABLE_OBJC_WEAK = YES; 400 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 401 | CLANG_WARN_BOOL_CONVERSION = YES; 402 | CLANG_WARN_COMMA = YES; 403 | CLANG_WARN_CONSTANT_CONVERSION = YES; 404 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 405 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 406 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 407 | CLANG_WARN_EMPTY_BODY = YES; 408 | CLANG_WARN_ENUM_CONVERSION = YES; 409 | CLANG_WARN_INFINITE_RECURSION = YES; 410 | CLANG_WARN_INT_CONVERSION = YES; 411 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 412 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 413 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 414 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 415 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 416 | CLANG_WARN_STRICT_PROTOTYPES = YES; 417 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 418 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 419 | CLANG_WARN_UNREACHABLE_CODE = YES; 420 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 421 | COPY_PHASE_STRIP = NO; 422 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 423 | ENABLE_NS_ASSERTIONS = NO; 424 | ENABLE_STRICT_OBJC_MSGSEND = YES; 425 | GCC_C_LANGUAGE_STANDARD = gnu11; 426 | GCC_NO_COMMON_BLOCKS = YES; 427 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 428 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 429 | GCC_WARN_UNDECLARED_SELECTOR = YES; 430 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 431 | GCC_WARN_UNUSED_FUNCTION = YES; 432 | GCC_WARN_UNUSED_VARIABLE = YES; 433 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 434 | MTL_ENABLE_DEBUG_INFO = NO; 435 | MTL_FAST_MATH = YES; 436 | SDKROOT = iphoneos; 437 | SWIFT_COMPILATION_MODE = wholemodule; 438 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 439 | VALIDATE_PRODUCT = YES; 440 | }; 441 | name = Release; 442 | }; 443 | 84252F8823B56C9E0016D338 /* Debug */ = { 444 | isa = XCBuildConfiguration; 445 | baseConfigurationReference = CCAD255B48F722FEB9F90FB3 /* Pods-RTSPViewer.debug.xcconfig */; 446 | buildSettings = { 447 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 448 | CODE_SIGN_STYLE = Automatic; 449 | INFOPLIST_FILE = RTSPViewer/Info.plist; 450 | LD_RUNPATH_SEARCH_PATHS = ( 451 | "$(inherited)", 452 | "@executable_path/Frameworks", 453 | ); 454 | PRODUCT_BUNDLE_IDENTIFIER = com.RTSPViewer; 455 | PRODUCT_NAME = "$(TARGET_NAME)"; 456 | SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/$(PROJECT_NAME)/$(PROJECT_NAME)-Bridging-Header.h"; 457 | SWIFT_VERSION = 5.0; 458 | TARGETED_DEVICE_FAMILY = "1,2"; 459 | }; 460 | name = Debug; 461 | }; 462 | 84252F8923B56C9E0016D338 /* Release */ = { 463 | isa = XCBuildConfiguration; 464 | baseConfigurationReference = D86C30A2D8F688A7B77ED438 /* Pods-RTSPViewer.release.xcconfig */; 465 | buildSettings = { 466 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 467 | CODE_SIGN_STYLE = Automatic; 468 | INFOPLIST_FILE = RTSPViewer/Info.plist; 469 | LD_RUNPATH_SEARCH_PATHS = ( 470 | "$(inherited)", 471 | "@executable_path/Frameworks", 472 | ); 473 | PRODUCT_BUNDLE_IDENTIFIER = com.RTSPViewer; 474 | PRODUCT_NAME = "$(TARGET_NAME)"; 475 | SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/$(PROJECT_NAME)/$(PROJECT_NAME)-Bridging-Header.h"; 476 | SWIFT_VERSION = 5.0; 477 | TARGETED_DEVICE_FAMILY = "1,2"; 478 | }; 479 | name = Release; 480 | }; 481 | /* End XCBuildConfiguration section */ 482 | 483 | /* Begin XCConfigurationList section */ 484 | 84252F6E23B56C990016D338 /* Build configuration list for PBXProject "RTSPViewer" */ = { 485 | isa = XCConfigurationList; 486 | buildConfigurations = ( 487 | 84252F8523B56C9E0016D338 /* Debug */, 488 | 84252F8623B56C9E0016D338 /* Release */, 489 | ); 490 | defaultConfigurationIsVisible = 0; 491 | defaultConfigurationName = Release; 492 | }; 493 | 84252F8723B56C9E0016D338 /* Build configuration list for PBXNativeTarget "RTSPViewer" */ = { 494 | isa = XCConfigurationList; 495 | buildConfigurations = ( 496 | 84252F8823B56C9E0016D338 /* Debug */, 497 | 84252F8923B56C9E0016D338 /* Release */, 498 | ); 499 | defaultConfigurationIsVisible = 0; 500 | defaultConfigurationName = Release; 501 | }; 502 | /* End XCConfigurationList section */ 503 | }; 504 | rootObject = 84252F6B23B56C990016D338 /* Project object */; 505 | } 506 | -------------------------------------------------------------------------------- /RTSPViewer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /RTSPViewer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RTSPViewer.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /RTSPViewer.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RTSPViewer/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2019-12-26. 6 | // Copyright © 2019 mani. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /RTSPViewer/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /RTSPViewer/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /RTSPViewer/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 | -------------------------------------------------------------------------------- /RTSPViewer/Cells/TextInputCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextInputCell.swift 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2020-01-05. 6 | // Copyright © 2020 mani. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TextInputCell: UITableViewCell { 12 | var keyboardType: UIKeyboardType = .default { 13 | didSet { 14 | textField.keyboardType = keyboardType 15 | } 16 | } 17 | var returnKeyType: UIReturnKeyType = .default { 18 | didSet { 19 | textField.returnKeyType = returnKeyType 20 | } 21 | } 22 | let textField = UITextField() 23 | 24 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 25 | super.init(style: style, reuseIdentifier: reuseIdentifier) 26 | 27 | initialize() 28 | } 29 | 30 | required init?(coder aDecoder: NSCoder) { 31 | super.init(coder: aDecoder) 32 | 33 | initialize() 34 | } 35 | 36 | private func initialize() { 37 | textField.clearButtonMode = .whileEditing 38 | textField.translatesAutoresizingMaskIntoConstraints = false 39 | 40 | contentView.addSubview(textField) 41 | } 42 | 43 | override func layoutSubviews() { 44 | super.layoutSubviews() 45 | 46 | let margins = self.layoutMarginsGuide 47 | 48 | NSLayoutConstraint.activate([ 49 | textField.topAnchor.constraint(equalTo: contentView.topAnchor), 50 | textField.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 51 | textField.leadingAnchor.constraint(equalTo: margins.leadingAnchor), 52 | textField.trailingAnchor.constraint(equalTo: margins.trailingAnchor) 53 | ]) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /RTSPViewer/Cells/VideoCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoCell.swift 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2020-01-07. 6 | // Copyright © 2020 mani. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class VideoCell: UICollectionViewCell { 12 | var videoView = VideoView(frame: .zero) 13 | var checkMarkView = SSCheckMarkView(frame: .zero) 14 | override var isSelected: Bool { 15 | didSet { 16 | checkMarkView.checked = isSelected 17 | } 18 | } 19 | 20 | override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | 23 | initialize() 24 | } 25 | 26 | required init?(coder: NSCoder) { 27 | super.init(coder: coder) 28 | 29 | initialize() 30 | } 31 | 32 | private func initialize() { 33 | videoView.translatesAutoresizingMaskIntoConstraints = false 34 | videoView.aspectRatio = "1:1" 35 | 36 | checkMarkView.translatesAutoresizingMaskIntoConstraints = false 37 | checkMarkView.checked = false 38 | checkMarkView.backgroundColor = .clear 39 | 40 | contentView.addSubview(videoView) 41 | contentView.addSubview(checkMarkView) 42 | } 43 | 44 | override func layoutSubviews() { 45 | super.layoutSubviews() 46 | 47 | NSLayoutConstraint.activate([ 48 | videoView.topAnchor.constraint(equalTo: contentView.topAnchor), 49 | videoView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 50 | videoView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 51 | videoView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) 52 | ]) 53 | 54 | NSLayoutConstraint.activate([ 55 | checkMarkView.heightAnchor.constraint(equalToConstant: 35.0), 56 | checkMarkView.widthAnchor.constraint(equalToConstant: 35.0), 57 | checkMarkView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 58 | checkMarkView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) 59 | ]) 60 | } 61 | 62 | func playStream(at url: URL) { 63 | videoView.loadVideo(from: url) 64 | } 65 | 66 | func stopStream() { 67 | videoView.unloadVideo() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /RTSPViewer/Controllers/AddStreamViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddStreamViewController.swift 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2020-01-01. 6 | // Copyright © 2020 mani. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AddStreamViewController: UITableViewController { 12 | @IBOutlet weak var urlCell: TextInputCell! 13 | var handler: ((URL) -> Void)? 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | title = "Add Stream" 19 | } 20 | 21 | override func viewDidAppear(_ animated: Bool) { 22 | super.viewDidAppear(animated) 23 | 24 | setupTextInputCell() 25 | 26 | urlCell.textField.becomeFirstResponder() 27 | } 28 | } 29 | 30 | extension AddStreamViewController { 31 | // MARK: - Setup 32 | 33 | private func setupTextInputCell() { 34 | let textField = urlCell.textField 35 | 36 | textField.delegate = self 37 | textField.addTarget(self, action: #selector(textDidChange), for: .editingChanged) 38 | textField.autocapitalizationType = .none 39 | textField.keyboardType = .URL 40 | textField.returnKeyType = .done 41 | } 42 | } 43 | 44 | extension AddStreamViewController { 45 | // MARK: - Text field validation and changes 46 | 47 | func handleTextfieldValidation(in textField: UITextField, message: String) { 48 | textField.text = "" 49 | let placeholderTextColor = UIColor(red: 236.0/255.0, green: 75.0/255.0, blue: 75.0/255.0, alpha: 1.0) 50 | textField.attributedPlaceholder = NSAttributedString(string: message, 51 | attributes: 52 | [NSAttributedString.Key.foregroundColor: placeholderTextColor]) 53 | textField.textColor = .red 54 | textField.shake() 55 | 56 | textField.becomeFirstResponder() 57 | } 58 | 59 | @objc func textDidChange(sender: UITextField) { 60 | if sender.textColor != .black { 61 | sender.textColor = .black 62 | } 63 | } 64 | } 65 | 66 | extension AddStreamViewController { 67 | // MARK: - IBAction 68 | 69 | @IBAction func done() { 70 | let textField = urlCell.textField 71 | let validator = Validator() 72 | 73 | if textField.validate([validator.isURLValid]) == false { 74 | handleTextfieldValidation(in: textField, 75 | message: "Please enter a valid URL") 76 | } else { 77 | textField.resignFirstResponder() 78 | } 79 | 80 | if let url = textField.text { 81 | if let url = URL(string: url) { 82 | dismiss(animated: true) { [weak self] in 83 | guard let weakSelf = self else { return } 84 | weakSelf.handler?(url) 85 | } 86 | } 87 | } 88 | } 89 | 90 | @IBAction func cancel(sender: UIBarButtonItem) { 91 | dismiss(animated: true, completion: nil) 92 | } 93 | } 94 | 95 | extension AddStreamViewController: UITextFieldDelegate { 96 | // MARK: - Text field delegate 97 | 98 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 99 | if textField.returnKeyType == .done { 100 | done() 101 | return true 102 | } 103 | return false 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /RTSPViewer/Controllers/RTSPStreamViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RTSPStreamViewController.swift 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2019-12-26. 6 | // Copyright © 2019 mani. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private let reuseIdentifier = "RTSPStreamCell" 12 | 13 | class RTSPStreamViewController: UICollectionViewController { 14 | 15 | private var videoStreams = [URL]() 16 | private var deleteButtonItem: UIBarButtonItem! 17 | @IBOutlet weak var selectionModeButtonItem: UIBarButtonItem! 18 | var isSelecting: Bool = false { 19 | didSet { 20 | collectionView.allowsMultipleSelection = isSelecting 21 | navigationController?.setToolbarHidden(!isSelecting, animated: true) 22 | 23 | updateInterfaceForSelectionMode() 24 | } 25 | } 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | 30 | collectionView.dragInteractionEnabled = true 31 | collectionView.dragDelegate = self 32 | collectionView.dropDelegate = self 33 | 34 | deleteButtonItem = UIBarButtonItem(barButtonSystemItem: .trash, 35 | target: self, 36 | action: #selector(deleteSelectedItems)) 37 | deleteButtonItem.isEnabled = false 38 | 39 | setToolbarItems([deleteButtonItem], animated: true) 40 | 41 | title = "Live View" 42 | } 43 | } 44 | 45 | extension RTSPStreamViewController { 46 | // MARK: - Adding and deleting streams 47 | 48 | func add(stream: URL) { 49 | videoStreams.append(stream) 50 | 51 | videoStreams.sort { $0.absoluteString < $1.absoluteString } 52 | 53 | if let index = self.videoStreams.firstIndex(of: stream) { 54 | let indexPath = IndexPath(row: index, section: 0) 55 | self.collectionView.insertItems(at: [indexPath]) 56 | self.collectionView.reloadItems(at: [indexPath]) 57 | } 58 | } 59 | 60 | @objc func deleteSelectedItems() { 61 | if let selectedPaths = collectionView.indexPathsForSelectedItems { 62 | let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) 63 | 64 | let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) 65 | 66 | let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { [unowned self] _ in 67 | self.deleteItems(at: selectedPaths) 68 | self.updateInterfaceForSelectionMode() 69 | } 70 | 71 | alertController.addAction(cancelAction) 72 | alertController.addAction(deleteAction) 73 | 74 | present(alertController, animated: true) 75 | } 76 | } 77 | 78 | func deleteItems(at indexPaths: [IndexPath]) { 79 | var deleteVideoStreams = [URL]() 80 | indexPaths.forEach { indexPath in 81 | deleteVideoStreams.append(videoStreams[indexPath.row]) 82 | } 83 | 84 | videoStreams.removeElements(in: deleteVideoStreams) 85 | 86 | collectionView.performBatchUpdates({ 87 | self.collectionView.deleteItems(at: indexPaths) 88 | }, completion: nil) 89 | 90 | if videoStreams.count == 0 { 91 | toggleSelection() 92 | } 93 | } 94 | } 95 | 96 | extension RTSPStreamViewController { 97 | // MARK: - Item Selection 98 | 99 | @IBAction func toggleSelection() { 100 | if isSelecting == true { 101 | selectionModeButtonItem.title = "Select" 102 | collectionView.deselectAllItems(animated: true) 103 | isSelecting = false 104 | } else { 105 | selectionModeButtonItem.title = "Cancel" 106 | isSelecting = true 107 | } 108 | } 109 | 110 | func updateInterfaceForSelectionMode() { 111 | guard let selectedPaths = collectionView.indexPathsForSelectedItems else { 112 | return 113 | } 114 | 115 | let enableButtonItem = (selectedPaths.count > 0) 116 | deleteButtonItem.isEnabled = enableButtonItem 117 | 118 | switch selectedPaths.count { 119 | case let selectionCount where selectionCount == 1: 120 | title = "\(selectedPaths.count) item selected" 121 | case let selectionCount where selectionCount > 1: 122 | title = "\(selectedPaths.count) items selected" 123 | default: 124 | title = isSelecting ? "Select Items" : "Live View" 125 | } 126 | } 127 | } 128 | 129 | extension RTSPStreamViewController { 130 | // MARK: Collection view data source 131 | 132 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 133 | return 1 134 | } 135 | 136 | 137 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 138 | return videoStreams.count 139 | } 140 | 141 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 142 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! VideoCell 143 | 144 | let videoStreamURL = videoStreams[indexPath.row] 145 | cell.playStream(at: videoStreamURL) 146 | 147 | return cell 148 | } 149 | } 150 | 151 | extension RTSPStreamViewController { 152 | // MARK: - Collection view delegate 153 | 154 | override func collectionView(_ collectionView: UICollectionView, 155 | shouldSelectItemAt indexPath: IndexPath) -> Bool { 156 | return true 157 | } 158 | 159 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 160 | guard isSelecting else { return } 161 | 162 | updateInterfaceForSelectionMode() 163 | } 164 | 165 | override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { 166 | guard isSelecting else { return } 167 | 168 | updateInterfaceForSelectionMode() 169 | } 170 | } 171 | 172 | extension RTSPStreamViewController: UICollectionViewDelegateFlowLayout { 173 | // MARK: - Collection view flow layout delegate 174 | 175 | func collectionView(_ collectionView: UICollectionView, 176 | layout collectionViewLayout: UICollectionViewLayout, 177 | sizeForItemAt indexPath: IndexPath) -> CGSize { 178 | let cellDimensions = CGSize(width: view.frame.width / 2, 179 | height: view.frame.width / 2) 180 | return cellDimensions 181 | } 182 | } 183 | 184 | extension RTSPStreamViewController: UICollectionViewDragDelegate { 185 | // MARK: - Collection view drag delegate 186 | 187 | func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, 188 | at indexPath: IndexPath) -> [UIDragItem] { 189 | let url = videoStreams[indexPath.row] 190 | 191 | let itemProvider = NSItemProvider(object: url as NSURL) 192 | let dragItem = UIDragItem(itemProvider: itemProvider) 193 | return [dragItem] 194 | } 195 | } 196 | 197 | extension RTSPStreamViewController: UICollectionViewDropDelegate { 198 | // MARK: - Collection view drop delegate 199 | 200 | func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool { 201 | return true 202 | } 203 | 204 | func collectionView(_ collectionView: UICollectionView, 205 | performDropWith coordinator: UICollectionViewDropCoordinator) { 206 | guard let destinationIndexPath = coordinator.destinationIndexPath else { 207 | return 208 | } 209 | 210 | coordinator.items.forEach { dropItem in 211 | guard let sourceIndexPath = dropItem.sourceIndexPath else { 212 | return 213 | } 214 | 215 | collectionView.performBatchUpdates({ 216 | let toMoveStream = videoStreams.remove(at: sourceIndexPath.row) 217 | videoStreams.insert(toMoveStream, at: destinationIndexPath.row) 218 | 219 | collectionView.deleteItems(at: [sourceIndexPath]) 220 | collectionView.insertItems(at: [destinationIndexPath]) 221 | }, completion: { _ in 222 | coordinator.drop(dropItem.dragItem, 223 | toItemAt: destinationIndexPath) 224 | }) 225 | } 226 | } 227 | 228 | func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, 229 | withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal { 230 | if collectionView.hasActiveDrag { 231 | return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) 232 | } 233 | return UICollectionViewDropProposal(operation: .forbidden) 234 | } 235 | } 236 | 237 | extension RTSPStreamViewController { 238 | // MARK: - Segue 239 | 240 | override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { 241 | if identifier == "showVideoStream" && isSelecting == true { 242 | return false 243 | } 244 | return true 245 | } 246 | 247 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 248 | switch segue.identifier { 249 | case "addStream": 250 | addStream(for: segue) 251 | case "showVideoStream": 252 | showVideoStream(for: segue) 253 | default: 254 | preconditionFailure("Segue identifier did not match") 255 | } 256 | } 257 | 258 | private func addStream(for segue: UIStoryboardSegue) { 259 | guard let navController = segue.destination as? UINavigationController else { return } 260 | guard let topController = navController.topViewController else { return } 261 | guard let viewController = topController as? AddStreamViewController else { return } 262 | 263 | viewController.handler = { [unowned self] (url) in 264 | self.add(stream: url) 265 | } 266 | } 267 | 268 | private func showVideoStream(for segue: UIStoryboardSegue) { 269 | if let selectedIndexPaths = collectionView.indexPathsForSelectedItems, 270 | let selectedItem = selectedIndexPaths.first { 271 | collectionView.deselectItem(at: selectedItem, animated: false) 272 | guard let navController = segue.destination as? UINavigationController else { return } 273 | guard let videoStreamController = navController.topViewController as? 274 | VideoStreamController else { return } 275 | let authorizedStream = videoStreams[selectedItem.row] 276 | videoStreamController.videoStreamURL = authorizedStream 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /RTSPViewer/Controllers/VideoStreamController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoStreamController.swift 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2020-01-26. 6 | // Copyright © 2020 mani. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class VideoStreamController: UIViewController { 12 | @IBOutlet weak var videoView: VideoView! 13 | var videoStreamURL: URL? { 14 | willSet { 15 | if let url = newValue { 16 | if let videoView = videoView { 17 | videoView.loadVideo(from: url) 18 | } 19 | } 20 | } 21 | } 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | guard let url = videoStreamURL else { return } 27 | 28 | videoView.textLabel.font = UIFont.systemFont(ofSize: 40.0) 29 | 30 | title = url.host 31 | 32 | navigationController?.setNavigationBarHidden(true, animated: false) 33 | 34 | videoView.loadVideo(from: url) 35 | } 36 | 37 | override func viewWillAppear(_ animated: Bool) { 38 | super.viewWillAppear(animated) 39 | 40 | let value = UIInterfaceOrientation.landscapeLeft.rawValue 41 | UIDevice.current.setValue(value, forKey: "orientation") 42 | } 43 | } 44 | 45 | extension VideoStreamController { 46 | // MARK: Device Orientation 47 | 48 | override var supportedInterfaceOrientations: UIInterfaceOrientationMask { 49 | return .landscape 50 | } 51 | 52 | override var shouldAutorotate: Bool { 53 | return true 54 | } 55 | } 56 | 57 | extension VideoStreamController { 58 | // MARK: IBActions 59 | 60 | @IBAction func close(sender: UIBarButtonItem) { 61 | dismiss(animated: true, completion: nil) 62 | } 63 | 64 | @IBAction func handleTap(gesture: UITapGestureRecognizer) { 65 | if let hidden = navigationController?.isNavigationBarHidden { 66 | navigationController?.setNavigationBarHidden(!hidden, animated: true) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /RTSPViewer/Extensions/Array+EXT.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+EXT.swift 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2020-01-25. 6 | // Copyright © 2020 mani. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Array where Element: Equatable { 12 | mutating func remove(element: Element) { 13 | if let index = firstIndex(of: element) { 14 | remove(at: index) 15 | } 16 | } 17 | 18 | mutating func removeElements(in collection: [Element]) { 19 | collection.forEach { remove(element: $0) } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /RTSPViewer/Extensions/String+EXT.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+EXT.swift 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2020-01-05. 6 | // Copyright © 2020 mani. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String: Evaluatable { 12 | func evaluate(with condition: String) -> Bool { 13 | guard let range = range(of: condition, options: .regularExpression, range: nil, locale: nil) else { 14 | return false 15 | } 16 | 17 | return range.lowerBound == startIndex && range.upperBound == endIndex 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /RTSPViewer/Extensions/UICollectionView+EXT.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+EXT.swift 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2020-01-25. 6 | // Copyright © 2020 mani. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UICollectionView { 12 | func deselectAllItems(animated: Bool) { 13 | indexPathsForSelectedItems?.forEach { deselectItem(at: $0, animated: true) } 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /RTSPViewer/Extensions/UITextField+EXT.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITextField+EXT.swift 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2020-01-05. 6 | // Copyright © 2020 mani. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UITextField { 12 | func shake() { 13 | let shake = CABasicAnimation(keyPath: "position") 14 | shake.duration = 0.1 15 | shake.repeatCount = 2 16 | shake.autoreverses = true 17 | 18 | shake.fromValue = [self.center.x - 6, self.center.y] 19 | shake.toValue = [self.center.x + 6, self.center.y] 20 | self.layer.add(shake, forKey: "position") 21 | } 22 | } 23 | 24 | extension UITextField: Validatable { 25 | func validate(_ functions: [(String) -> Bool]) -> Bool { 26 | return functions.map { $0(self.text ?? "") }.allSatisfy { $0 == true } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /RTSPViewer/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIMainStoryboardFile 45 | Main 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /RTSPViewer/Protocols/Evaluatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Evaluatable.swift 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2020-01-05. 6 | // Copyright © 2020 mani. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol Evaluatable { 12 | associatedtype Expression 13 | 14 | func evaluate(with condition: Expression) -> Bool 15 | } 16 | -------------------------------------------------------------------------------- /RTSPViewer/Protocols/Validatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Validatable.swift 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2020-01-05. 6 | // Copyright © 2020 mani. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol Validatable { 12 | associatedtype Validators 13 | 14 | func validate(_ functions: [Validators]) -> Bool 15 | } 16 | -------------------------------------------------------------------------------- /RTSPViewer/RTSPViewer-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // RTSPViewer-Bridging-Header.h 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2020-01-07. 6 | // Copyright © 2020 mani. All rights reserved. 7 | // 8 | 9 | #import 10 | -------------------------------------------------------------------------------- /RTSPViewer/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2019-12-26. 6 | // Copyright © 2019 mani. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 20 | guard let _ = (scene as? UIWindowScene) else { return } 21 | } 22 | 23 | func sceneDidDisconnect(_ scene: UIScene) { 24 | // Called as the scene is being released by the system. 25 | // This occurs shortly after the scene enters the background, or when its session is discarded. 26 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 27 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 28 | } 29 | 30 | func sceneDidBecomeActive(_ scene: UIScene) { 31 | // Called when the scene has moved from an inactive state to an active state. 32 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 33 | } 34 | 35 | func sceneWillResignActive(_ scene: UIScene) { 36 | // Called when the scene will move from an active state to an inactive state. 37 | // This may occur due to temporary interruptions (ex. an incoming phone call). 38 | } 39 | 40 | func sceneWillEnterForeground(_ scene: UIScene) { 41 | // Called as the scene transitions from the background to the foreground. 42 | // Use this method to undo the changes made on entering the background. 43 | } 44 | 45 | func sceneDidEnterBackground(_ scene: UIScene) { 46 | // Called as the scene transitions from the foreground to the background. 47 | // Use this method to save data, release shared resources, and store enough scene-specific state information 48 | // to restore the scene back to its current state. 49 | } 50 | 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /RTSPViewer/Storyboards/AddStream.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /RTSPViewer/Storyboards/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /RTSPViewer/Storyboards/VideoStream.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /RTSPViewer/Validator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Validator.swift 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2020-01-05. 6 | // Copyright © 2020 mani. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Validator { 12 | func isURLValid(text: String) -> Bool { 13 | let regexp = "(?i)(http?|https|rtsp)://(?:www\\.)?\\S+(?:/|\\b)" 14 | return text.evaluate(with: regexp) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /RTSPViewer/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2019-12-26. 6 | // Copyright © 2019 home. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view. 16 | } 17 | 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /RTSPViewer/Views/SSCheckMarkView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SSCheckMarkView.swift 3 | // CameraViewer 4 | // 5 | // Created by mani on 2019-09-08. 6 | // Copyright © 2019 mani. All rights reserved. 7 | // 8 | // https://stackoverflow.com/questions/18977527/how-do-i-display-the-standard-checkmark-on-a-uicollectionviewcell 9 | 10 | import UIKit 11 | 12 | class SSCheckMarkView: UIView { 13 | enum SSCheckMarkStyle: UInt { 14 | case openCircle 15 | case grayedOut 16 | } 17 | 18 | var checked: Bool = false { 19 | didSet { 20 | setNeedsDisplay() 21 | } 22 | } 23 | 24 | var checkMarkStyle: SSCheckMarkStyle = .grayedOut { 25 | didSet { 26 | setNeedsDisplay() 27 | } 28 | } 29 | 30 | override func draw(_ rect: CGRect) { 31 | super.draw(rect) 32 | 33 | if checked { 34 | drawCheckedRect(rect: rect) 35 | } 36 | } 37 | 38 | func drawCheckedRect(rect: CGRect) { 39 | let context = UIGraphicsGetCurrentContext() 40 | let checkmarkBlue2 = UIColor(red: 0.078, green: 0.435, blue: 0.875, alpha: 1) 41 | let shadow2 = UIColor.black 42 | 43 | let shadow2Offset = CGSize(width: 0.1, height: -0.1) 44 | let shadow2BlurRadius = 2.5 45 | let frame = self.bounds 46 | let group = CGRect(x: frame.minX + 3, 47 | y: frame.minY + 3, 48 | width: frame.width - 6, 49 | height: frame.height - 6) 50 | 51 | let checkedOvalPath = UIBezierPath(ovalIn: CGRect(x: group.minX + floor(group.width * 0.00000 + 0.5), 52 | y: group.minY + floor(group.height * 0.00000 + 0.5), 53 | width: floor(group.width * 1.00000 + 0.5) - 54 | floor(group.width * 0.00000 + 0.5), 55 | height: floor(group.height * 1.00000 + 0.5) - 56 | floor(group.height * 0.00000 + 0.5))) 57 | 58 | context!.saveGState() 59 | context!.setShadow(offset: shadow2Offset, blur: CGFloat(shadow2BlurRadius), color: shadow2.cgColor) 60 | checkmarkBlue2.setFill() 61 | checkedOvalPath.fill() 62 | context!.restoreGState() 63 | UIColor.white.setStroke() 64 | checkedOvalPath.lineWidth = 1 65 | checkedOvalPath.stroke() 66 | 67 | let bezierPath = UIBezierPath() 68 | bezierPath.move(to: CGPoint(x: group.minX + 0.27083 * group.width, y: group.minY + 0.54167 * group.height)) 69 | bezierPath.addLine(to: CGPoint(x: group.minX + 0.41667 * group.width, y: group.minY + 0.68750 * group.height)) 70 | bezierPath.addLine(to: CGPoint(x: group.minX + 0.75000 * group.width, y: group.minY + 0.35417 * group.height)) 71 | bezierPath.lineCapStyle = CGLineCap.square 72 | UIColor.white.setStroke() 73 | bezierPath.lineWidth = 1.3 74 | bezierPath.stroke() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /RTSPViewer/Views/VideoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoView.swift 3 | // RTSPViewer 4 | // 5 | // Created by mani on 2020-01-07. 6 | // Copyright © 2020 mani. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class VideoView: UIView { 12 | let textLabel = UILabel(frame: .zero) 13 | let mediaPlayer = VLCMediaPlayer() 14 | var isMuted: Bool = true { 15 | willSet { 16 | mediaPlayer.audio.volume = (newValue == true) ? 0 : 100 17 | } 18 | } 19 | var aspectRatio: String = "16:9" { 20 | willSet { 21 | mediaPlayer.videoAspectRatio = UnsafeMutablePointer(mutating: (newValue as NSString).utf8String) 22 | } 23 | } 24 | 25 | override init(frame: CGRect) { 26 | super.init(frame: frame) 27 | 28 | initialize() 29 | } 30 | 31 | required init?(coder aDecoder: NSCoder) { 32 | super.init(coder: aDecoder) 33 | 34 | initialize() 35 | } 36 | 37 | private func initialize() { 38 | mediaPlayer.drawable = self 39 | 40 | textLabel.text = "Loading video..." 41 | textLabel.font = UIFont.systemFont(ofSize: 18.0) 42 | textLabel.textColor = .white 43 | 44 | addSubview(textLabel) 45 | } 46 | 47 | override func layoutSubviews() { 48 | textLabel.translatesAutoresizingMaskIntoConstraints = false 49 | NSLayoutConstraint.activate([ 50 | textLabel.centerYAnchor.constraint(equalTo: self.safeAreaLayoutGuide.centerYAnchor), 51 | textLabel.centerXAnchor.constraint(equalTo: self.safeAreaLayoutGuide.centerXAnchor) 52 | ]) 53 | } 54 | 55 | func loadVideo(from url: URL) { 56 | let media = VLCMedia(url: url) 57 | mediaPlayer.media = media 58 | isMuted = true 59 | mediaPlayer.play() 60 | } 61 | 62 | func unloadVideo() { 63 | mediaPlayer.stop() 64 | } 65 | } 66 | --------------------------------------------------------------------------------