├── Examples ├── .gitignore ├── ControlPointDemo_ObjC │ ├── ControlPointDemo │ │ ├── ControlPointDemo-Bridging-Header.h │ │ ├── Images.xcassets │ │ │ ├── LaunchImage.launchimage │ │ │ │ └── Contents.json │ │ │ ├── play_button.imageset │ │ │ │ ├── play_button.png │ │ │ │ ├── play_button@2x.png │ │ │ │ ├── play_button@3x.png │ │ │ │ └── Contents.json │ │ │ ├── stop_button.imageset │ │ │ │ ├── stop_button.png │ │ │ │ ├── stop_button@2x.png │ │ │ │ ├── stop_button@3x.png │ │ │ │ └── Contents.json │ │ │ ├── pause_button.imageset │ │ │ │ ├── pause_button.png │ │ │ │ ├── pause_button@2x.png │ │ │ │ ├── pause_button@3x.png │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── RootFolderViewController.h │ │ ├── AppDelegate.h │ │ ├── main.m │ │ ├── Info.plist │ │ ├── FolderViewController.h │ │ ├── Player.h │ │ ├── Base.lproj │ │ │ └── LaunchScreen.xib │ │ ├── AppDelegate.m │ │ ├── FolderViewController.m │ │ └── Player.m │ ├── Podfile │ ├── ControlPointDemo.xcodeproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── ControlPointDemo.xcscheme │ ├── ControlPointDemo.xcworkspace │ │ └── contents.xcworkspacedata │ └── Podfile.lock ├── ControlPointDemo_Swift │ ├── ControlPointDemo │ │ ├── Images.xcassets │ │ │ ├── play_button.imageset │ │ │ │ ├── play_button.png │ │ │ │ ├── play_button@2x.png │ │ │ │ ├── play_button@3x.png │ │ │ │ └── Contents.json │ │ │ ├── stop_button.imageset │ │ │ │ ├── stop_button.png │ │ │ │ ├── stop_button@2x.png │ │ │ │ ├── stop_button@3x.png │ │ │ │ └── Contents.json │ │ │ ├── pause_button.imageset │ │ │ │ ├── pause_button.png │ │ │ │ ├── pause_button@2x.png │ │ │ │ ├── pause_button@3x.png │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── Base.lproj │ │ │ └── LaunchScreen.xib │ │ ├── AppDelegate.swift │ │ ├── FolderViewController.swift │ │ └── Player.swift │ ├── Podfile │ ├── ControlPointDemo.xcodeproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── ControlPointDemo.xcscheme │ ├── ControlPointDemo.xcworkspace │ │ └── contents.xcworkspacedata │ └── Podfile.lock └── ControlPointDemoMacOS_Swift │ ├── Podfile │ ├── ControlPointDemo.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── ControlPointDemo.xcscheme │ ├── ControlPointDemo.xcworkspace │ └── contents.xcworkspacedata │ ├── ControlPointDemo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ └── ViewController.swift │ └── Podfile.lock ├── .gitignore ├── LICENSE ├── Source ├── UPnP Foundation │ ├── SSDPDiscovery.swift │ ├── SSDPType.swift │ ├── UniqueServiceName.swift │ └── GlobalLib.swift ├── UPnP Objects │ ├── UPnPEvent.swift │ ├── UPnPArchivable.swift │ └── AbstractUPnP.swift ├── HTTP Client Session Managers and Serializers │ ├── SOAPSessionManager.swift │ ├── SOAPSerialization.swift │ └── UPnPEventSubscriptionSerialization.swift ├── Parsers │ ├── AbstractDOMXMLParser.swift │ ├── SAXXMLParserElementObservation.swift │ └── AbstractSAXXMLParser.swift ├── AV Profile │ ├── Devices │ │ ├── MediaServer1Device.swift │ │ └── MediaRenderer1Device.swift │ ├── Events │ │ └── AVTransport1Event.swift │ └── Services │ │ ├── ConnectionManager1Service.swift │ │ └── ContentDirectory1Objects.swift ├── SSDP │ ├── Discovery Adapter │ │ ├── SSDPDiscoveryAdapter.swift │ │ └── SSDPExplorerDiscoveryAdapter.swift │ └── Explorer │ │ └── SSDPExplorer.swift ├── UPnAtom.swift ├── Logging │ ├── Logger.swift │ └── PropertyPrinter.swift └── CocoaSSDP Support │ └── CocoaSSDPDiscoveryAdapter.swift ├── UPnAtom.podspec ├── .travis.yml └── README.md /Examples/.gitignore: -------------------------------------------------------------------------------- 1 | Pods/ -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/ControlPointDemo-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | 4 | ], 5 | "info" : { 6 | "version" : 1, 7 | "author" : "xcode" 8 | } 9 | } -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/play_button.imageset/play_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/play_button.imageset/play_button.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/stop_button.imageset/stop_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/stop_button.imageset/stop_button.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/play_button.imageset/play_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/play_button.imageset/play_button.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/stop_button.imageset/stop_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/stop_button.imageset/stop_button.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/pause_button.imageset/pause_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/pause_button.imageset/pause_button.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/play_button.imageset/play_button@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/play_button.imageset/play_button@2x.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/play_button.imageset/play_button@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/play_button.imageset/play_button@3x.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/stop_button.imageset/stop_button@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/stop_button.imageset/stop_button@2x.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/stop_button.imageset/stop_button@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/stop_button.imageset/stop_button@3x.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/pause_button.imageset/pause_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/pause_button.imageset/pause_button.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/play_button.imageset/play_button@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/play_button.imageset/play_button@2x.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/play_button.imageset/play_button@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/play_button.imageset/play_button@3x.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/stop_button.imageset/stop_button@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/stop_button.imageset/stop_button@2x.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/stop_button.imageset/stop_button@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/stop_button.imageset/stop_button@3x.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/pause_button.imageset/pause_button@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/pause_button.imageset/pause_button@2x.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/pause_button.imageset/pause_button@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/pause_button.imageset/pause_button@3x.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/Podfile: -------------------------------------------------------------------------------- 1 | inhibit_all_warnings! 2 | use_frameworks! 3 | source 'https://github.com/CocoaPods/Specs.git' 4 | 5 | project 'ControlPointDemo' 6 | 7 | target 'ControlPointDemo' do 8 | pod 'UPnAtom', :path => '../..' 9 | end 10 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/pause_button.imageset/pause_button@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/pause_button.imageset/pause_button@2x.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/pause_button.imageset/pause_button@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-nevi/UPnAtom/HEAD/Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/pause_button.imageset/pause_button@3x.png -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/Podfile: -------------------------------------------------------------------------------- 1 | inhibit_all_warnings! 2 | use_frameworks! 3 | source 'https://github.com/CocoaPods/Specs.git' 4 | 5 | project 'ControlPointDemo' 6 | 7 | target 'ControlPointDemo' do 8 | pod 'UPnAtom', :path => '../..' 9 | end 10 | -------------------------------------------------------------------------------- /Examples/ControlPointDemoMacOS_Swift/Podfile: -------------------------------------------------------------------------------- 1 | inhibit_all_warnings! 2 | use_frameworks! 3 | source 'https://github.com/CocoaPods/Specs.git' 4 | 5 | project 'ControlPointDemo' 6 | 7 | target 'ControlPointDemo' do 8 | pod 'UPnAtom', :path => '../..' 9 | end 10 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/ControlPointDemoMacOS_Swift/ControlPointDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Examples/ControlPointDemoMacOS_Swift/ControlPointDemo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/pause_button.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "pause_button.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "pause_button@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "pause_button@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/play_button.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "play_button.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "play_button@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "play_button@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/stop_button.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "stop_button.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "stop_button@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "stop_button@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/play_button.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "play_button.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "play_button@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "play_button@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/stop_button.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "stop_button.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "stop_button@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "stop_button@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/pause_button.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "pause_button.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "pause_button@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "pause_button@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | # CocoaPods 21 | # 22 | # We recommend against adding the Pods directory to your .gitignore. However 23 | # you should judge for yourself, the pros and cons are mentioned at: 24 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 25 | # 26 | # Pods/ 27 | -------------------------------------------------------------------------------- /Examples/ControlPointDemoMacOS_Swift/ControlPointDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ControlPointDemo 4 | // 5 | // Created by David Robles on 8/21/16. 6 | // Copyright © 2016 David Robles. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | 15 | 16 | func applicationDidFinishLaunching(aNotification: NSNotification) { 17 | // Insert code here to initialize your application 18 | } 19 | 20 | func applicationWillTerminate(aNotification: NSNotification) { 21 | // Insert code here to tear down your application 22 | } 23 | 24 | 25 | } 26 | 27 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 master-nevi 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 | 23 | -------------------------------------------------------------------------------- /Examples/ControlPointDemoMacOS_Swift/ControlPointDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Examples/ControlPointDemoMacOS_Swift/ControlPointDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2016 David Robles. All rights reserved. 29 | NSMainStoryboardFile 30 | Main 31 | NSPrincipalClass 32 | NSApplication 33 | NSAppTransportSecurity 34 | 35 | NSAllowsArbitraryLoads 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/RootFolderViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // RootFolderViewController.h 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | @import UIKit; 25 | 26 | @interface RootFolderViewController : UIViewController 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | @import UIKit; 25 | 26 | @interface AppDelegate : UIResponder 27 | 28 | @property (strong, nonatomic) UIWindow *window; 29 | 30 | 31 | @end 32 | 33 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | @import UIKit; 25 | #import "AppDelegate.h" 26 | 27 | int main(int argc, char * argv[]) { 28 | @autoreleasepool { 29 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | NSAppTransportSecurity 40 | 41 | NSAllowsArbitraryLoads 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | NSAppTransportSecurity 40 | 41 | NSAllowsArbitraryLoads 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/FolderViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // FolderViewController.h 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | @import UIKit; 25 | 26 | @class MediaServer1Device; 27 | 28 | @interface FolderViewController : UIViewController 29 | 30 | - (void)configureWithDevice:(MediaServer1Device *)device header:(NSString *)header rootId:(NSString *)rootId; 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /Source/UPnP Foundation/SSDPDiscovery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SSDPDiscovery.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | struct SSDPDiscovery { 25 | let usn: UniqueServiceName 26 | let descriptionURL: NSURL 27 | let type: SSDPType 28 | } 29 | 30 | extension SSDPDiscovery: Equatable { } 31 | 32 | func ==(lhs: SSDPDiscovery, rhs: SSDPDiscovery) -> Bool { 33 | return lhs.usn == rhs.usn 34 | } 35 | -------------------------------------------------------------------------------- /Examples/ControlPointDemoMacOS_Swift/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AFNetworking (3.1.0): 3 | - AFNetworking/NSURLSession (= 3.1.0) 4 | - AFNetworking/Reachability (= 3.1.0) 5 | - AFNetworking/Security (= 3.1.0) 6 | - AFNetworking/Serialization (= 3.1.0) 7 | - AFNetworking/UIKit (= 3.1.0) 8 | - AFNetworking/NSURLSession (3.1.0): 9 | - AFNetworking/Reachability 10 | - AFNetworking/Security 11 | - AFNetworking/Serialization 12 | - AFNetworking/Reachability (3.1.0) 13 | - AFNetworking/Security (3.1.0) 14 | - AFNetworking/Serialization (3.1.0) 15 | - CocoaAsyncSocket (7.4.3): 16 | - CocoaAsyncSocket/All (= 7.4.3) 17 | - CocoaAsyncSocket/All (7.4.3): 18 | - CocoaAsyncSocket/GCD 19 | - CocoaAsyncSocket/RunLoop 20 | - CocoaAsyncSocket/GCD (7.4.3) 21 | - CocoaAsyncSocket/RunLoop (7.4.3) 22 | - GCDWebServer (3.3.3): 23 | - GCDWebServer/Core (= 3.3.3) 24 | - GCDWebServer/Core (3.3.3) 25 | - Ono (1.2.2) 26 | - UPnAtom (0.7.0): 27 | - AFNetworking (~> 3.1) 28 | - CocoaAsyncSocket (~> 7.4.1) 29 | - GCDWebServer (~> 3.3) 30 | - Ono (~> 1.2.0) 31 | 32 | DEPENDENCIES: 33 | - UPnAtom (from `../..`) 34 | 35 | EXTERNAL SOURCES: 36 | UPnAtom: 37 | :path: "../.." 38 | 39 | SPEC CHECKSUMS: 40 | AFNetworking: 5e0e199f73d8626b11e79750991f5d173d1f8b67 41 | CocoaAsyncSocket: a18c75dca4b08723628a0bacca6e94803d90be91 42 | GCDWebServer: 1c39a1f0763e4eb492bee021e4270fce097d3555 43 | Ono: 80b1c686777d3f6d5d5ce6fcac8a74afef4ca197 44 | UPnAtom: 3c96820e2f784fb45116c9cd0e2feebb4f960c87 45 | 46 | PODFILE CHECKSUM: 0c01f4122f5037d4cf76b9400cce28aeada874ce 47 | 48 | COCOAPODS: 1.0.1 49 | -------------------------------------------------------------------------------- /Source/UPnP Objects/UPnPEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UPnPEvent.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | 26 | /// TODO: For now rooting to NSObject to expose to Objective-C, see Github issue #16 27 | public class UPnPEvent: NSObject { 28 | public let eventXML: NSData 29 | public weak var service: AbstractUPnPService? 30 | init(eventXML: NSData, service: AbstractUPnPService) { 31 | self.eventXML = eventXML 32 | self.service = service 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AFNetworking (3.1.0): 3 | - AFNetworking/NSURLSession (= 3.1.0) 4 | - AFNetworking/Reachability (= 3.1.0) 5 | - AFNetworking/Security (= 3.1.0) 6 | - AFNetworking/Serialization (= 3.1.0) 7 | - AFNetworking/UIKit (= 3.1.0) 8 | - AFNetworking/NSURLSession (3.1.0): 9 | - AFNetworking/Reachability 10 | - AFNetworking/Security 11 | - AFNetworking/Serialization 12 | - AFNetworking/Reachability (3.1.0) 13 | - AFNetworking/Security (3.1.0) 14 | - AFNetworking/Serialization (3.1.0) 15 | - AFNetworking/UIKit (3.1.0): 16 | - AFNetworking/NSURLSession 17 | - CocoaAsyncSocket (7.4.3): 18 | - CocoaAsyncSocket/All (= 7.4.3) 19 | - CocoaAsyncSocket/All (7.4.3): 20 | - CocoaAsyncSocket/GCD 21 | - CocoaAsyncSocket/RunLoop 22 | - CocoaAsyncSocket/GCD (7.4.3) 23 | - CocoaAsyncSocket/RunLoop (7.4.3) 24 | - GCDWebServer (3.3.3): 25 | - GCDWebServer/Core (= 3.3.3) 26 | - GCDWebServer/Core (3.3.3) 27 | - Ono (1.2.2) 28 | - UPnAtom (0.7.0): 29 | - AFNetworking (~> 3.1) 30 | - CocoaAsyncSocket (~> 7.4.1) 31 | - GCDWebServer (~> 3.3) 32 | - Ono (~> 1.2.0) 33 | 34 | DEPENDENCIES: 35 | - UPnAtom (from `../..`) 36 | 37 | EXTERNAL SOURCES: 38 | UPnAtom: 39 | :path: "../.." 40 | 41 | SPEC CHECKSUMS: 42 | AFNetworking: 5e0e199f73d8626b11e79750991f5d173d1f8b67 43 | CocoaAsyncSocket: a18c75dca4b08723628a0bacca6e94803d90be91 44 | GCDWebServer: 1c39a1f0763e4eb492bee021e4270fce097d3555 45 | Ono: 80b1c686777d3f6d5d5ce6fcac8a74afef4ca197 46 | UPnAtom: 3c96820e2f784fb45116c9cd0e2feebb4f960c87 47 | 48 | PODFILE CHECKSUM: 0c01f4122f5037d4cf76b9400cce28aeada874ce 49 | 50 | COCOAPODS: 1.0.1 51 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AFNetworking (3.1.0): 3 | - AFNetworking/NSURLSession (= 3.1.0) 4 | - AFNetworking/Reachability (= 3.1.0) 5 | - AFNetworking/Security (= 3.1.0) 6 | - AFNetworking/Serialization (= 3.1.0) 7 | - AFNetworking/UIKit (= 3.1.0) 8 | - AFNetworking/NSURLSession (3.1.0): 9 | - AFNetworking/Reachability 10 | - AFNetworking/Security 11 | - AFNetworking/Serialization 12 | - AFNetworking/Reachability (3.1.0) 13 | - AFNetworking/Security (3.1.0) 14 | - AFNetworking/Serialization (3.1.0) 15 | - AFNetworking/UIKit (3.1.0): 16 | - AFNetworking/NSURLSession 17 | - CocoaAsyncSocket (7.4.3): 18 | - CocoaAsyncSocket/All (= 7.4.3) 19 | - CocoaAsyncSocket/All (7.4.3): 20 | - CocoaAsyncSocket/GCD 21 | - CocoaAsyncSocket/RunLoop 22 | - CocoaAsyncSocket/GCD (7.4.3) 23 | - CocoaAsyncSocket/RunLoop (7.4.3) 24 | - GCDWebServer (3.3.3): 25 | - GCDWebServer/Core (= 3.3.3) 26 | - GCDWebServer/Core (3.3.3) 27 | - Ono (1.2.2) 28 | - UPnAtom (0.7.0): 29 | - AFNetworking (~> 3.1) 30 | - CocoaAsyncSocket (~> 7.4.1) 31 | - GCDWebServer (~> 3.3) 32 | - Ono (~> 1.2.0) 33 | 34 | DEPENDENCIES: 35 | - UPnAtom (from `../..`) 36 | 37 | EXTERNAL SOURCES: 38 | UPnAtom: 39 | :path: "../.." 40 | 41 | SPEC CHECKSUMS: 42 | AFNetworking: 5e0e199f73d8626b11e79750991f5d173d1f8b67 43 | CocoaAsyncSocket: a18c75dca4b08723628a0bacca6e94803d90be91 44 | GCDWebServer: 1c39a1f0763e4eb492bee021e4270fce097d3555 45 | Ono: 80b1c686777d3f6d5d5ce6fcac8a74afef4ca197 46 | UPnAtom: 3c96820e2f784fb45116c9cd0e2feebb4f960c87 47 | 48 | PODFILE CHECKSUM: 0c01f4122f5037d4cf76b9400cce28aeada874ce 49 | 50 | COCOAPODS: 1.0.1 51 | -------------------------------------------------------------------------------- /Source/HTTP Client Session Managers and Serializers/SOAPSessionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SOAPSessionManager.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | import AFNetworking 26 | 27 | public class SOAPSessionManager: AFHTTPSessionManager { 28 | override init(baseURL url: NSURL?, sessionConfiguration configuration: NSURLSessionConfiguration?) { 29 | super.init(baseURL: url, sessionConfiguration: configuration) 30 | 31 | self.requestSerializer = SOAPRequestSerializer() 32 | self.responseSerializer = SOAPResponseSerializer() 33 | } 34 | 35 | required public init?(coder aDecoder: NSCoder) { 36 | super.init(coder: aDecoder) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Player.h: -------------------------------------------------------------------------------- 1 | // 2 | // Player.h 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | @import Foundation; 25 | @import UIKit; 26 | 27 | @class MediaServer1Device; 28 | @class MediaRenderer1Device; 29 | 30 | @interface Player : NSObject 31 | 32 | + (Player *)sharedInstance; 33 | 34 | - (void)startPlayback:(NSArray *)playList position:(NSInteger)position; 35 | - (void)startPlayback:(NSInteger)position; 36 | 37 | @property (nonatomic, readonly) NSArray *playlist; 38 | @property (nonatomic) MediaServer1Device *mediaServer; 39 | @property (nonatomic) MediaRenderer1Device *mediaRenderer; 40 | @property (nonatomic, readonly) UIBarButtonItem *playPauseButton; 41 | @property (nonatomic, readonly) UIBarButtonItem *stopButton; 42 | 43 | @end 44 | 45 | -------------------------------------------------------------------------------- /Source/Parsers/AbstractDOMXMLParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AbstractDOMXMLParser.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | import Ono 26 | 27 | public class AbstractDOMXMLParser { 28 | public func parse(data data: NSData) -> EmptyResult { 29 | LogVerbose("Parsing XML:\nSTART\n\(NSString(data: data, encoding: NSUTF8StringEncoding))\nEND") 30 | 31 | var parserResult: EmptyResult = .Failure(createError("Parser failure")) 32 | autoreleasepool { () -> () in 33 | do { 34 | parserResult = self.parse(document:try ONOXMLDocument(data: data)) 35 | } catch let parseError as NSError { 36 | parserResult = EmptyResult.Failure(parseError) 37 | } 38 | } 39 | 40 | return parserResult 41 | } 42 | 43 | public func parse(document document: ONOXMLDocument) -> EmptyResult { 44 | fatalError("Implement in subclass") 45 | } 46 | 47 | // MARK: - Internal lib 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Source/AV Profile/Devices/MediaServer1Device.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaServer1Device.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | 26 | public class MediaServer1Device: AbstractUPnPDevice { 27 | public var avTransportService: AVTransport1Service? { 28 | return service(forURN: "urn:schemas-upnp-org:service:AVTransport:1") as? AVTransport1Service 29 | } 30 | 31 | public var connectionManagerService: ConnectionManager1Service? { 32 | return service(forURN: "urn:schemas-upnp-org:service:ConnectionManager:1") as? ConnectionManager1Service 33 | } 34 | 35 | public var contentDirectoryService: ContentDirectory1Service? { 36 | return service(forURN: "urn:schemas-upnp-org:service:ContentDirectory:1") as? ContentDirectory1Service 37 | } 38 | } 39 | 40 | /// for objective-c type checking 41 | extension AbstractUPnP { 42 | public func isMediaServer1Device() -> Bool { 43 | return self is MediaServer1Device 44 | } 45 | } 46 | 47 | /// overrides ExtendedPrintable protocol implementation 48 | extension MediaServer1Device { 49 | override public var className: String { return "\(self.dynamicType)" } 50 | override public var description: String { 51 | var properties = PropertyPrinter() 52 | properties.add(super.className, property: super.description) 53 | return properties.description 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Source/AV Profile/Devices/MediaRenderer1Device.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaRenderer1Device.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | 26 | public class MediaRenderer1Device: AbstractUPnPDevice { 27 | public var avTransportService: AVTransport1Service? { 28 | return service(forURN: "urn:schemas-upnp-org:service:AVTransport:1") as? AVTransport1Service 29 | } 30 | 31 | public var connectionManagerService: ConnectionManager1Service? { 32 | return service(forURN: "urn:schemas-upnp-org:service:ConnectionManager:1") as? ConnectionManager1Service 33 | } 34 | 35 | public var renderingControlService: RenderingControl1Service? { 36 | return service(forURN: "urn:schemas-upnp-org:service:RenderingControl:1") as? RenderingControl1Service 37 | } 38 | } 39 | 40 | /// for objective-c type checking 41 | extension AbstractUPnP { 42 | public func isMediaRenderer1Device() -> Bool { 43 | return self is MediaRenderer1Device 44 | } 45 | } 46 | 47 | /// overrides ExtendedPrintable protocol implementation 48 | extension MediaRenderer1Device { 49 | override public var className: String { return "\(self.dynamicType)" } 50 | override public var description: String { 51 | var properties = PropertyPrinter() 52 | properties.add(super.className, property: super.description) 53 | return properties.description 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Source/SSDP/Discovery Adapter/SSDPDiscoveryAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVTransport1Event.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | protocol SSDPDiscoveryAdapterDelegate: class { 25 | func ssdpDiscoveryAdapter(adapter: SSDPDiscoveryAdapter, didUpdateSSDPDiscoveries ssdpDiscoveries: [SSDPDiscovery]) 26 | /// Assume discovery adapter has stopped after a failure. 27 | func ssdpDiscoveryAdapter(adapter: SSDPDiscoveryAdapter, didFailWithError error: NSError) 28 | } 29 | 30 | /// Provides an interface to allow any SSDP library to be used an adapted into UPnAtom for SSDP discovery. 31 | protocol SSDPDiscoveryAdapter: class { 32 | var rawSSDPTypes: Set { get set } 33 | weak var delegate: SSDPDiscoveryAdapterDelegate? { get set } 34 | var running: Bool { get } 35 | func start() 36 | func stop() 37 | func restart() 38 | } 39 | 40 | /// An abstract class to allow any SSDP library to be used an adapted into UPnAtom for SSDP discovery. 41 | class AbstractSSDPDiscoveryAdapter: SSDPDiscoveryAdapter { 42 | var rawSSDPTypes: Set = [] 43 | weak var delegate: SSDPDiscoveryAdapterDelegate? 44 | var delegateQueue = dispatch_get_main_queue() 45 | private(set) var running = false 46 | 47 | required init() { } 48 | 49 | func start() { 50 | running = true 51 | } 52 | 53 | func stop() { 54 | running = false 55 | } 56 | 57 | func restart() { 58 | if running { 59 | stop() 60 | } 61 | start() 62 | } 63 | 64 | /// 🔰 = protected 65 | /// 66 | /// Sets running = false. 67 | func failed🔰() { 68 | running = false 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /UPnAtom.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'UPnAtom' 3 | s.version = '0.7.0' 4 | s.license = { :type => 'MIT', :file => 'LICENSE' } 5 | s.summary = 'An open source Universal Plug and Play library with a focus on media streaming coordination using the UPnP A/V profile.' 6 | s.homepage = 'https://github.com/master-nevi/UPnAtom' 7 | s.authors = { 'David Robles' => 'master-nevi@users.noreply.github.com' } 8 | s.source = { :git => 'https://github.com/master-nevi/UPnAtom.git', :tag => s.version.to_s } 9 | s.requires_arc = true 10 | s.ios.deployment_target = '8.0' 11 | s.osx.deployment_target = '10.9' 12 | 13 | s.source_files = 'Source/**/*.swift' 14 | s.exclude_files = 'Source/CocoaSSDP Support/*.swift' 15 | s.dependency 'CocoaAsyncSocket', '~> 7.4.1' # UPnP object discovery using SSDP 16 | s.dependency 'AFNetworking', '~> 3.1' # HTTP Client 17 | s.dependency 'Ono', '~> 1.2.0' # XML parsing 18 | s.dependency 'GCDWebServer', '~> 3.3' # UPnP event notification handling 19 | 20 | 21 | # NOTE: I really did try to be a good pod architect and modularize the library into subspecs, however there are still bugs in the Swift compiler which cause it to crash when building release/archive versions of UPnAtom. Because the problem areas may consistantly or inconsistanly cause the compiler crash, the fewer subspecs or no subspecs the better when it comes to tracking them down (i.e. less hair being pulled out). I look forward to doing it in the future however. Here's a sneak peek: 22 | 23 | # s.default_subspecs = 'Default' 24 | 25 | # s.subspec 'Default' do |ss| 26 | # ss.source_files = 'Source/UPnAtom.swift' 27 | # ss.dependency 'UPnAtom/AV Profile' 28 | # end 29 | 30 | # s.subspec 'UPnP Foundation' do |ss| 31 | # ss.source_files = 'Source/UPnP Foundation/*.swift' 32 | # end 33 | 34 | # s.subspec 'SSDP Explorer' do |ss| 35 | # ss.source_files = 'Source/SSDP/Explorer/*.swift' 36 | # ss.dependency 'UPnAtom/UPnP Foundation' 37 | # ss.dependency 'CocoaAsyncSocket', '~> 7.4.1' # UPnP object discovery using SSDP 38 | # ss.dependency 'AFNetworking', '~> 2.5.0' # HTTP Client 39 | # end 40 | 41 | # s.subspec 'Core' do |ss| 42 | # ss.source_files = 'Source/{HTTP Client Session Managers and Serializers,Logging,Management,Parsers,UPnP Objects}/*.swift', 'Source/SSDP/Discovery Adapter/*.swift' 43 | # ss.dependency 'UPnAtom/SSDP Explorer' 44 | # ss.dependency 'Ono', '~> 1.2.0' # XML parsing 45 | # ss.dependency 'GCDWebServer', '~> 3.2.2' # UPnP event notification handling 46 | # end 47 | 48 | # s.subspec 'AV Profile' do |ss| 49 | # ss.source_files = 'Source/AV Profile/**/*.swift' 50 | # ss.dependency 'UPnAtom/Core' 51 | # end 52 | 53 | # s.subspec 'CocoaSSDP Support' do |ss| 54 | # ss.source_files = 'Source/CocoaSSDP Support/*.swift' 55 | # ss.dependency 'UPnAtom/Core' 56 | # ss.dependency 'CocoaSSDP', '~> 0.1.0' 57 | # end 58 | end 59 | -------------------------------------------------------------------------------- /Source/Parsers/SAXXMLParserElementObservation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SAXXMLParserElementObservation.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | 26 | public class SAXXMLParserElementObservation { 27 | // internal 28 | private(set) var didStartParsingElement: ((elementName: String, attributeDict: [NSObject : AnyObject]!) -> Void)? // TODO: Should ideally be a constant, see Github issue #10 29 | let didEndParsingElement: ((elementName: String) -> Void)? 30 | let foundInnerText: ((elementName: String, text: String) -> Void)? 31 | let elementPath: [String] 32 | private(set) var innerText: String? 33 | 34 | public init(elementPath: [String], didStartParsingElement: ((elementName: String, attributeDict: [NSObject : AnyObject]!) -> Void)?, didEndParsingElement: ((elementName: String) -> Void)?, foundInnerText: ((elementName: String, text: String) -> Void)?) { 35 | self.elementPath = elementPath 36 | 37 | self.didEndParsingElement = didEndParsingElement 38 | self.foundInnerText = foundInnerText 39 | 40 | self.didStartParsingElement = {[unowned self] (elementName: String, attributeDict: [NSObject : AnyObject]!) -> Void in 41 | // reset _innerText at the start of the element parse 42 | self.innerText = nil 43 | 44 | if let didStartParsingElement = didStartParsingElement { 45 | didStartParsingElement(elementName: elementName, attributeDict: attributeDict) 46 | } 47 | } 48 | } 49 | 50 | func appendInnerText(innerText: String?) { 51 | if self.innerText == nil { 52 | self.innerText = "" 53 | } 54 | 55 | if let innerText = innerText { 56 | self.innerText! += innerText 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | #import "AppDelegate.h" 25 | 26 | @interface AppDelegate () 27 | 28 | @end 29 | 30 | @implementation AppDelegate 31 | 32 | 33 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 34 | 35 | // Override point for customization after application launch. 36 | return YES; 37 | } 38 | 39 | - (void)applicationWillResignActive:(UIApplication *)application { 40 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 41 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 42 | } 43 | 44 | - (void)applicationDidEnterBackground:(UIApplication *)application { 45 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 46 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 47 | } 48 | 49 | - (void)applicationWillEnterForeground:(UIApplication *)application { 50 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 51 | } 52 | 53 | - (void)applicationDidBecomeActive:(UIApplication *)application { 54 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 55 | } 56 | 57 | - (void)applicationWillTerminate:(UIApplication *)application { 58 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 59 | } 60 | 61 | @end 62 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode7.3 3 | env: 4 | global: 5 | - LC_CTYPE=en_US.UTF-8 6 | - LANG=en_US.UTF-8 7 | - OBJC_IOS_EXAMPLE_DIR="Examples/ControlPointDemo_ObjC" 8 | - SWIFT_IOS_EXAMPLE_DIR="Examples/ControlPointDemo_Swift" 9 | - SWIFT_MACOS_EXAMPLE_DIR="Examples/ControlPointDemoMacOS_Swift" 10 | - WORKSPACE=ControlPointDemo.xcworkspace 11 | - IOS_SDK=iphonesimulator9.3 12 | - OSX_SDK=macosx10.11 13 | - TVOS_SDK=appletvsimulator9.2 14 | - WATCHOS_SDK=watchsimulator2.2 15 | - EXAMPLE_SCHEME="ControlPointDemo" 16 | matrix: 17 | - DESTINATION="OS=8.1,name=iPhone 4S" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="YES" 18 | - DESTINATION="OS=8.2,name=iPhone 5" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO" 19 | - DESTINATION="OS=8.3,name=iPhone 5S" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO" 20 | - DESTINATION="OS=8.4,name=iPhone 6" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO" 21 | - DESTINATION="OS=9.0,name=iPhone 6" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO" 22 | - DESTINATION="OS=9.1,name=iPhone 6 Plus" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO" 23 | - DESTINATION="OS=9.2,name=iPhone 6S" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO" 24 | - DESTINATION="OS=9.3,name=iPhone 6S Plus" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO" 25 | - DESTINATION="arch=x86_64" SCHEME="$OSX_FRAMEWORK_SCHEME" SDK="$OSX_SDK" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO" 26 | script: 27 | - set -o pipefail 28 | - xcodebuild -version 29 | - xcodebuild -showsdks 30 | - pod repo update 31 | 32 | # Build Example in Debug if specified 33 | - if [ $BUILD_EXAMPLE == "YES" ] && [ $SDK == $IOS_SDK ]; then 34 | pod install --project-directory="$OBJC_IOS_EXAMPLE_DIR"; 35 | xcodebuild -workspace "$OBJC_IOS_EXAMPLE_DIR/$WORKSPACE" -scheme "$EXAMPLE_SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty -c; 36 | fi 37 | 38 | # Build Example in Debug if specified 39 | - if [ $BUILD_EXAMPLE == "YES" ] && [ $SDK == $IOS_SDK ]; then 40 | pod install --project-directory="$SWIFT_IOS_EXAMPLE_DIR"; 41 | xcodebuild -workspace "$SWIFT_IOS_EXAMPLE_DIR/$WORKSPACE" -scheme "$EXAMPLE_SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty -c; 42 | fi 43 | 44 | # Build Example in Debug if specified 45 | - if [ $BUILD_EXAMPLE == "YES" ] && [ $SDK == $OSX_SDK ]; then 46 | pod install --project-directory="$SWIFT_MACOS_EXAMPLE_DIR"; 47 | xcodebuild -workspace "$SWIFT_MACOS_EXAMPLE_DIR/$WORKSPACE" -scheme "$EXAMPLE_SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty -c; 48 | fi 49 | 50 | # Run `pod lib lint` if specified 51 | - if [ $POD_LINT == "YES" ]; then 52 | pod lib lint --verbose --allow-warnings; 53 | fi -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import UIKit 25 | 26 | @UIApplicationMain 27 | class AppDelegate: UIResponder, UIApplicationDelegate { 28 | 29 | var window: UIWindow? 30 | 31 | 32 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 33 | // Override point for customization after application launch. 34 | return true 35 | } 36 | 37 | func applicationWillResignActive(application: UIApplication) { 38 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 39 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 40 | } 41 | 42 | func applicationDidEnterBackground(application: UIApplication) { 43 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 44 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 45 | } 46 | 47 | func applicationWillEnterForeground(application: UIApplication) { 48 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 49 | } 50 | 51 | func applicationDidBecomeActive(application: UIApplication) { 52 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 53 | } 54 | 55 | func applicationWillTerminate(application: UIApplication) { 56 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 57 | } 58 | 59 | 60 | } 61 | 62 | -------------------------------------------------------------------------------- /Source/UPnP Objects/UPnPArchivable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UPnPArchivable.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | 26 | public class UPnPArchivable: NSObject, NSCoding { 27 | public let usn: String 28 | public let descriptionURL: NSURL 29 | 30 | init(usn: String, descriptionURL: NSURL) { 31 | self.usn = usn 32 | self.descriptionURL = descriptionURL 33 | } 34 | 35 | required public init?(coder decoder: NSCoder) { 36 | self.usn = decoder.decodeObjectForKey("usn") as! String 37 | self.descriptionURL = decoder.decodeObjectForKey("descriptionURL") as! NSURL 38 | } 39 | 40 | public func encodeWithCoder(coder: NSCoder) { 41 | coder.encodeObject(usn, forKey: "usn") 42 | coder.encodeObject(descriptionURL, forKey: "descriptionURL") 43 | } 44 | } 45 | 46 | extension AbstractUPnP { 47 | public func archivable() -> UPnPArchivable { 48 | return UPnPArchivable(usn: usn.rawValue, descriptionURL: descriptionURL) 49 | } 50 | } 51 | 52 | public class UPnPArchivableAnnex: UPnPArchivable { 53 | /// Use the custom metadata dictionary to re-populate any missing data fields from a custom device or service subclass. While it's not enforced by the compiler, the contents of the meta data must conform to the NSCoding protocol in order to be archivable. Avoided using Swift generics in order to allow compatability with Obj-C. 54 | public let customMetadata: [String: AnyObject] 55 | 56 | init(usn: String, descriptionURL: NSURL, customMetadata: [String: AnyObject]) { 57 | self.customMetadata = customMetadata 58 | super.init(usn: usn, descriptionURL: descriptionURL) 59 | } 60 | 61 | required public init?(coder decoder: NSCoder) { 62 | self.customMetadata = decoder.decodeObjectForKey("customMetadata") as! [String: AnyObject] 63 | super.init(coder: decoder) 64 | } 65 | 66 | public override func encodeWithCoder(coder: NSCoder) { 67 | super.encodeWithCoder(coder) 68 | coder.encodeObject(customMetadata, forKey: "customMetadata") 69 | } 70 | } 71 | 72 | extension AbstractUPnP { 73 | public func archivable(customMetadata customMetadata: [String: AnyObject]) -> UPnPArchivableAnnex { 74 | return UPnPArchivableAnnex(usn: usn.rawValue, descriptionURL: descriptionURL, customMetadata: customMetadata) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![UPnAtom: Modern UPnP in Swift](https://raw.githubusercontent.com/master-nevi/UPnAtom/assets/UPnAtomLogo.png) 2 | [![Version](http://img.shields.io/cocoapods/v/UPnAtom.svg)](http://cocoapods.org/?q=UPnAtom) 3 | [![Platform](http://img.shields.io/cocoapods/p/UPnAtom.svg)](https://github.com/master-nevi/UPnAtom/blob/master/UPnAtom.podspec) 4 | [![License](http://img.shields.io/cocoapods/l/UPnAtom.svg)](https://github.com/master-nevi/UPnAtom/blob/master/LICENSE) 5 | [![Build Status](https://img.shields.io/travis/master-nevi/UPnAtom/master.svg)](https://travis-ci.org/master-nevi/UPnAtom) 6 | 7 | An open source Universal Plug and Play library with a focus on media streaming coordination using the UPnP A/V profile; written in Swift but for both Objective-C and Swift apps. Supports only iOS 8 and higher due to iOS 7's limitation of not supporting dynamic libraries via Clang module. 8 | 9 | ### Requirements: 10 | * iOS 8.0+ 11 | * OSX 10.9+ 12 | * Xcode 7.2 13 | 14 | ### Install: 15 | Add following to Podfile: 16 | ```ruby 17 | pod 'UPnAtom' 18 | ``` 19 | 20 | ### Usage: 21 | ###### Objective-C 22 | ```objective-c 23 | @import UPnAtom; 24 | ``` 25 | 26 | ###### Swift 27 | ```swift 28 | import UPnAtom 29 | ``` 30 | 31 | ###### More documentation is on the way. 32 | For now, it is highly recommended you check out the [example projects](https://github.com/master-nevi/UPnAtom/tree/master/Examples). They are exactly the same app however one is in Swift, and the other is in Objective-C. They demonstrate almost all of the library's features minus the ability to add your own UPnP service/device classes. If you create your own service/device classes simply register them following [UPnAtom.swift](https://github.com/master-nevi/UPnAtom/blob/master/Source/UPnAtom.swift) as an example. 33 | 34 | Note: On iOS, transport security has blocked cleartext HTTP (http://) resource loads since it is insecure. Since many, if not most, UPnP devices serve resources over http, temporary exceptions can be configured via your app's Info.plist file. Remove this restriction at your own risk. 35 | 36 | ### Milestones: 37 | * [x] Usable in both Swift and Objective-C projects via CocoaPod framework 38 | * [x] Create your own service and device object via class registration 39 | * [x] UPnP Version 1 Compliance 40 | * [x] Ability to archive UPnP objects after initial discovery and persist somewhere via NSCoder/NSCoding 41 | * [x] OSX 10.9+ support 42 | * [ ] Swift 2.0 43 | * [x] In-house implementation of SSDP discovery 44 | * [x] A/V Profile Feature parity with upnpx library 45 | * [ ] Documentation (Until then please check out the [example projects](https://github.com/master-nevi/UPnAtom/tree/master/Examples)) 46 | * [ ] UPnP Version 2 Compliance 47 | 48 | ### Tested Support On: 49 | ###### UPnP Servers: 50 | * [Kodi](http://kodi.tv/) - Open Source Home Theatre Software (aka XBMC) - [How to enable](http://kodi.wiki/view/UPnP/Server) 51 | * [Universal Media Server](http://www.universalmediaserver.com/) (fork of PS3 Media Server) 52 | 53 | ###### UPnP Clients: 54 | * [Kodi](http://kodi.tv/) - Open Source Home Theatre Software (aka XBMC) - [How to enable](http://kodi.wiki/view/UPnP/Client) 55 | * [Sony Bravia TV's with DLNA support](http://esupport.sony.com/p/support-info.pl?info_id=884&template_id=1®ion_id=8) 56 | 57 | ### Contribute: 58 | Currently I'm only taking feature requests, bugs, and bug fixes via [Github issue](https://github.com/master-nevi/UPnAtom/issues). Sorry no pull requests for features or major changes until the library is mature enough. 59 | 60 | - If you'd like to **ask a general question**, use [Stack Overflow](http://stackoverflow.com/). 61 | - If you **found a bug**, open an issue. 62 | - If you **have a feature request**, open an issue. 63 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo.xcodeproj/xcshareddata/xcschemes/ControlPointDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo.xcodeproj/xcshareddata/xcschemes/ControlPointDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Examples/ControlPointDemoMacOS_Swift/ControlPointDemo.xcodeproj/xcshareddata/xcschemes/ControlPointDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Source/UPnP Objects/AbstractUPnP.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AbstractUPnP.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | 26 | /// TODO: For now rooting to NSObject to expose to Objective-C, see Github issue #16 27 | public class AbstractUPnP: NSObject { 28 | public var uuid: String { 29 | return usn.uuid 30 | } 31 | public var urn: String { 32 | return usn.urn! // checked for nil during init 33 | } 34 | public let usn: UniqueServiceName 35 | public let descriptionURL: NSURL 36 | public var baseURL: NSURL! { 37 | return NSURL(string: "/", relativeToURL: descriptionURL)?.absoluteURL 38 | } 39 | 40 | required public init?(usn: UniqueServiceName, descriptionURL: NSURL, descriptionXML: NSData) { 41 | self.usn = usn 42 | self.descriptionURL = descriptionURL 43 | super.init() 44 | 45 | // only deal with UPnP object's with URN's for now, i.e. is either a device or service 46 | guard let urn = usn.urn where !urn.isEmpty else { 47 | return nil 48 | } 49 | } 50 | } 51 | 52 | public func ==(lhs: AbstractUPnP, rhs: AbstractUPnP) -> Bool { 53 | return lhs.usn == rhs.usn 54 | } 55 | 56 | extension AbstractUPnP { 57 | override public var hashValue: Int { 58 | return usn.hashValue 59 | } 60 | 61 | /// Because self is rooted to NSObject, for now, usage as a key in a dictionary will be treated as a key within an NSDictionary; which requires the overriding the methods hash and isEqual, see Github issue #16 62 | override public var hash: Int { 63 | return hashValue 64 | } 65 | 66 | /// Because self is rooted to NSObject, for now, usage as a key in a dictionary will be treated as a key within an NSDictionary; which requires the overriding the methods hash and isEqual, see Github issue #16 67 | override public func isEqual(object: AnyObject?) -> Bool { 68 | if let other = object as? AbstractUPnP { 69 | return self == other 70 | } 71 | return false 72 | } 73 | } 74 | 75 | extension AbstractUPnP: ExtendedPrintable { 76 | #if os(iOS) 77 | public var className: String { return "\(self.dynamicType)" } 78 | #elseif os(OSX) // NSObject.className actually exists on OSX! Who knew. 79 | override public var className: String { return "\(self.dynamicType)" } 80 | #endif 81 | override public var description: String { 82 | var properties = PropertyPrinter() 83 | properties.add("uuid", property: uuid) 84 | properties.add("urn", property: urn) 85 | properties.add("usn", property: usn.description) 86 | properties.add("descriptionURL", property: descriptionURL.absoluteString) 87 | properties.add("baseURL", property: baseURL.absoluteString) 88 | return properties.description 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Source/UPnAtom.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UPnAtom.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | 26 | /// TODO: For now rooting to NSObject to expose to Objective-C, see Github issue #16 27 | public class UPnAtom: NSObject { 28 | // public 29 | public static let sharedInstance = UPnAtom() 30 | public let upnpRegistry: UPnPRegistry 31 | public var ssdpTypes: Set { 32 | get { return ssdpDiscoveryAdapter.rawSSDPTypes } 33 | set { ssdpDiscoveryAdapter.rawSSDPTypes = newValue } 34 | } 35 | 36 | // internal 37 | unowned let ssdpDiscoveryAdapter: SSDPDiscoveryAdapter 38 | 39 | override init() { 40 | // configure discovery adapter 41 | let adapterClass = UPnAtom.ssdpDiscoveryAdapterClass() 42 | let adapter = adapterClass.init() 43 | ssdpDiscoveryAdapter = adapter 44 | 45 | // configure UPNP registry 46 | upnpRegistry = UPnPRegistry(ssdpDiscoveryAdapter: ssdpDiscoveryAdapter) 47 | for (upnpClass, urn) in UPnAtom.upnpClasses() { 48 | self.upnpRegistry.register(upnpClass: upnpClass, forURN: urn) 49 | } 50 | } 51 | 52 | deinit { 53 | ssdpDiscoveryAdapter.stop() 54 | } 55 | 56 | public func ssdpDiscoveryRunning() -> Bool { 57 | return ssdpDiscoveryAdapter.running 58 | } 59 | 60 | public func startSSDPDiscovery() { 61 | ssdpDiscoveryAdapter.start() 62 | } 63 | 64 | public func stopSSDPDiscovery() { 65 | ssdpDiscoveryAdapter.stop() 66 | } 67 | 68 | public func restartSSDPDiscovery() { 69 | ssdpDiscoveryAdapter.restart() 70 | } 71 | 72 | /// Override to use a different SSDP adapter if another SSDP system is preferred over CocoaSSDP 73 | class func ssdpDiscoveryAdapterClass() -> AbstractSSDPDiscoveryAdapter.Type { 74 | return SSDPExplorerDiscoveryAdapter.self 75 | } 76 | 77 | /// Override to use a different default set of UPnP classes. Alternatively, registrations can be replaced, see UPnAtom.upnpRegistry.register() 78 | class func upnpClasses() -> [(upnpClass: AbstractUPnP.Type, forURN: String)] { 79 | return [ 80 | (upnpClass: MediaRenderer1Device.self, forURN: "urn:schemas-upnp-org:device:MediaRenderer:1"), 81 | (upnpClass: MediaServer1Device.self, forURN: "urn:schemas-upnp-org:device:MediaServer:1"), 82 | (upnpClass: AVTransport1Service.self, forURN: "urn:schemas-upnp-org:service:AVTransport:1"), 83 | (upnpClass: ConnectionManager1Service.self, forURN: "urn:schemas-upnp-org:service:ConnectionManager:1"), 84 | (upnpClass: ContentDirectory1Service.self, forURN: "urn:schemas-upnp-org:service:ContentDirectory:1"), 85 | (upnpClass: RenderingControl1Service.self, forURN: "urn:schemas-upnp-org:service:RenderingControl:1") 86 | ] 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Examples/ControlPointDemoMacOS_Swift/ControlPointDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ControlPointDemo 4 | // 5 | // Created by David Robles on 8/21/16. 6 | // Copyright © 2016 David Robles. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import UPnAtom 11 | 12 | class ViewController: NSViewController { 13 | 14 | override func viewDidAppear() { 15 | super.viewDidAppear() 16 | 17 | NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.deviceWasAdded(_:)), name: UPnPRegistry.UPnPDeviceAddedNotification(), object: nil) 18 | NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.deviceWasRemoved(_:)), name: UPnPRegistry.UPnPDeviceRemovedNotification(), object: nil) 19 | NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.serviceWasAdded(_:)), name: UPnPRegistry.UPnPServiceAddedNotification(), object: nil) 20 | NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.serviceWasRemoved(_:)), name: UPnPRegistry.UPnPServiceRemovedNotification(), object: nil) 21 | 22 | UPnAtom.sharedInstance.ssdpTypes = [ 23 | SSDPTypeConstant.All.rawValue, 24 | SSDPTypeConstant.MediaServerDevice1.rawValue, 25 | SSDPTypeConstant.MediaRendererDevice1.rawValue, 26 | SSDPTypeConstant.ContentDirectory1Service.rawValue, 27 | SSDPTypeConstant.ConnectionManager1Service.rawValue, 28 | SSDPTypeConstant.RenderingControl1Service.rawValue, 29 | SSDPTypeConstant.AVTransport1Service.rawValue 30 | ] 31 | 32 | if !UPnAtom.sharedInstance.ssdpDiscoveryRunning() { 33 | UPnAtom.sharedInstance.startSSDPDiscovery() 34 | } 35 | } 36 | 37 | override func viewDidDisappear() { 38 | NSNotificationCenter.defaultCenter().removeObserver(self, name: UPnPRegistry.UPnPDeviceAddedNotification(), object: nil) 39 | NSNotificationCenter.defaultCenter().removeObserver(self, name: UPnPRegistry.UPnPDeviceRemovedNotification(), object: nil) 40 | NSNotificationCenter.defaultCenter().removeObserver(self, name: UPnPRegistry.UPnPServiceAddedNotification(), object: nil) 41 | NSNotificationCenter.defaultCenter().removeObserver(self, name: UPnPRegistry.UPnPServiceRemovedNotification(), object: nil) 42 | 43 | super.viewDidDisappear() 44 | } 45 | 46 | @objc private func deviceWasAdded(notification: NSNotification) { 47 | if let upnpDevice = notification.userInfo?[UPnPRegistry.UPnPDeviceKey()] as? AbstractUPnPDevice { 48 | print("Added device: \(upnpDevice.className) - \(upnpDevice.friendlyName)") 49 | } 50 | } 51 | 52 | @objc private func deviceWasRemoved(notification: NSNotification) { 53 | if let upnpDevice = notification.userInfo?[UPnPRegistry.UPnPDeviceKey()] as? AbstractUPnPDevice { 54 | print("Removed device: \(upnpDevice.className) - \(upnpDevice.friendlyName)") 55 | } 56 | } 57 | 58 | @objc private func serviceWasAdded(notification: NSNotification) { 59 | if let upnpService = notification.userInfo?[UPnPRegistry.UPnPServiceKey()] as? AbstractUPnPService { 60 | print("Added service: \(upnpService.className) - \(upnpService.descriptionURL)") 61 | 62 | if upnpService is AVTransport1Service { 63 | upnpService.addEventObserver(NSOperationQueue.currentQueue(), callBackBlock: { (event: UPnPEvent) -> Void in 64 | if let avTransportEvent = event as? AVTransport1Event { 65 | print("\(event.service?.className) Event: \(avTransportEvent.instanceState)") 66 | } 67 | }) 68 | } 69 | } 70 | } 71 | 72 | @objc private func serviceWasRemoved(notification: NSNotification) { 73 | if let upnpService = notification.userInfo?[UPnPRegistry.UPnPServiceKey()] as? AbstractUPnPService { 74 | print("Removed service: \(upnpService.className) - \(upnpService.descriptionURL)") 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Source/Logging/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | 26 | struct LogFlag : OptionSetType { 27 | let rawValue: UInt 28 | 29 | static let Error = LogFlag(rawValue: 1 << 0) // 0...00001 30 | static let Warning = LogFlag(rawValue: 1 << 1) // 0...00010 31 | static let Info = LogFlag(rawValue: 1 << 2) // 0...00100 32 | static let Debug = LogFlag(rawValue: 1 << 3) // 0...01000 33 | static let Verbose = LogFlag(rawValue: 1 << 4) // 0...10000 34 | } 35 | 36 | /// Debug levels 37 | extension LogFlag { 38 | static let OffLevel: LogFlag = [] 39 | static let ErrorLevel: LogFlag = [.Error] 40 | static let WarningLevel: LogFlag = [.Error, .Warning] 41 | static let InfoLevel: LogFlag = [.Error, .Warning, .Info] 42 | static let DebugLevel: LogFlag = [.Error, .Warning, .Info, .Debug] 43 | static let VerboseLevel: LogFlag = [.Error, .Warning, .Info, .Debug, .Verbose] 44 | 45 | } 46 | 47 | let defaultLogLevel = LogFlag.DebugLevel 48 | 49 | var logLevel = defaultLogLevel 50 | 51 | func resetDefaultDebugLevel() { 52 | logLevel = defaultLogLevel 53 | } 54 | 55 | func Log(isAsynchronous: Bool, level: LogFlag, flag: LogFlag, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, @autoclosure string: () -> String) { 56 | if level.contains(flag) { 57 | print(string()) 58 | } 59 | } 60 | 61 | func LogDebug(@autoclosure logText: () -> String, level: LogFlag = logLevel, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, asynchronous async: Bool = true) { 62 | Log(async, level: level, flag: .Debug, file: file, function: function, line: line, string: logText) 63 | } 64 | 65 | func LogInfo(@autoclosure logText: () -> String, level: LogFlag = logLevel, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, asynchronous async: Bool = true) { 66 | Log(async, level: level, flag: .Info, file: file, function: function, line: line, string: logText) 67 | } 68 | 69 | func LogWarn(@autoclosure logText: () -> String, level: LogFlag = logLevel, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, asynchronous async: Bool = true) { 70 | Log(async, level: level, flag: .Warning, file: file, function: function, line: line, string: logText) 71 | } 72 | 73 | func LogVerbose(@autoclosure logText: () -> String, level: LogFlag = logLevel, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, asynchronous async: Bool = true) { 74 | Log(async, level: level, flag: .Verbose, file: file, function: function, line: line, string: logText) 75 | } 76 | 77 | func LogError(@autoclosure logText: () -> String, level: LogFlag = logLevel, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, asynchronous async: Bool = false) { 78 | Log(async, level: level, flag: .Error, file: file, function: function, line: line, string: logText) 79 | } 80 | -------------------------------------------------------------------------------- /Source/UPnP Foundation/SSDPType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SSDPType.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | public enum SSDPTypeConstant: String { 25 | // General 26 | case All = "ssdp:all" 27 | case RootDevice = "upnp:rootdevice" 28 | 29 | // UPnP A/V profile 30 | case MediaServerDevice1 = "urn:schemas-upnp-org:device:MediaServer:1" 31 | case MediaRendererDevice1 = "urn:schemas-upnp-org:device:MediaRenderer:1" 32 | case ContentDirectory1Service = "urn:schemas-upnp-org:service:ContentDirectory:1" 33 | case ConnectionManager1Service = "urn:schemas-upnp-org:service:ConnectionManager:1" 34 | case RenderingControl1Service = "urn:schemas-upnp-org:service:RenderingControl:1" 35 | case AVTransport1Service = "urn:schemas-upnp-org:service:AVTransport:1" 36 | } 37 | 38 | enum SSDPType: RawRepresentable { 39 | case All 40 | case RootDevice 41 | case UUID(String) 42 | case Device(String) 43 | case Service(String) 44 | 45 | typealias RawValue = String 46 | 47 | init?(rawValue: RawValue) { 48 | if rawValue == "ssdp:all" { 49 | self = .All 50 | } else if rawValue == "upnp:rootdevice" { 51 | self = .RootDevice 52 | } else if rawValue.rangeOfString("uuid:") != nil { 53 | self = .UUID(rawValue) 54 | } else if rawValue.rangeOfString(":device:") != nil { 55 | self = .Device(rawValue) 56 | } else if rawValue.rangeOfString(":service:") != nil { 57 | self = .Service(rawValue) 58 | } else { 59 | return nil 60 | } 61 | } 62 | 63 | init?(typeConstant: SSDPTypeConstant) { 64 | self.init(rawValue: typeConstant.rawValue) 65 | } 66 | 67 | var rawValue: RawValue { 68 | switch self { 69 | case .All: 70 | return "ssdp:all" 71 | case .RootDevice: 72 | return "upnp:rootdevice" 73 | case .UUID(let rawValue): 74 | return rawValue 75 | case .Device(let rawValue): 76 | return rawValue 77 | case .Service(let rawValue): 78 | return rawValue 79 | } 80 | } 81 | } 82 | 83 | extension SSDPType: CustomStringConvertible { 84 | var description: String { 85 | return self.rawValue 86 | } 87 | } 88 | 89 | extension SSDPType: Hashable { 90 | var hashValue: Int { 91 | return self.rawValue.hashValue 92 | } 93 | } 94 | 95 | func ==(lhs: SSDPType, rhs: SSDPType) -> Bool { 96 | switch (lhs, rhs) { 97 | case (.All, .All): 98 | return true 99 | case (.RootDevice, .RootDevice): 100 | return true 101 | case (.UUID(let lhsRawValue), .UUID(let rhsRawValue)): 102 | return lhsRawValue == rhsRawValue 103 | case (.Device(let lhsRawValue), .Device(let rhsRawValue)): 104 | return lhsRawValue == rhsRawValue 105 | case (.Service(let lhsRawValue), .Service(let rhsRawValue)): 106 | return lhsRawValue == rhsRawValue 107 | default: 108 | return false 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/FolderViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FolderViewController.swift 3 | // ControlPointDemo 4 | // 5 | // Created by David Robles on 3/14/15. 6 | // Copyright (c) 2015 David Robles. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import UPnAtom 11 | 12 | class FolderViewController: UIViewController { 13 | @IBOutlet private weak var _tableView: UITableView! 14 | private var _playlist = [ContentDirectory1Object]() 15 | private var _mediaServer: MediaServer1Device! 16 | private var _contentDirectoryID: String! 17 | 18 | func configure(mediaServer mediaServer: MediaServer1Device, title: String, contentDirectoryID: String) { 19 | _mediaServer = mediaServer 20 | _contentDirectoryID = contentDirectoryID 21 | self.title = title 22 | } 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | _mediaServer?.contentDirectoryService?.getSortCapabilities({ (sortCapabilities: String?) -> Void in 28 | var sortCapabilities = sortCapabilities != nil ? sortCapabilities! : "" 29 | if sortCapabilities.rangeOfString("dc:title") != nil { 30 | sortCapabilities = "+dc:title" 31 | } 32 | 33 | self._mediaServer?.contentDirectoryService?.browse(objectID: self._contentDirectoryID, browseFlag: "BrowseDirectChildren", filter: "*", startingIndex: "0", requestedCount: "0", sortCriteria: sortCapabilities, success: { (result: [ContentDirectory1Object], numberReturned: Int, totalMatches: Int, updateID: String?) -> Void in 34 | self._playlist = result 35 | self._tableView.reloadData() 36 | }, failure: { (error: NSError) -> Void in 37 | print("Failed to browse content directory: \(error)") 38 | }) 39 | }, failure: { (error: NSError) -> Void in 40 | print("Failed to get sort capabilities: \(error)") 41 | }) 42 | 43 | let viewWidth = self.navigationController!.view.frame.size.width 44 | let titleLabel = UILabel(frame: CGRect(x: 0.0, y: 11.0, width: viewWidth - (viewWidth * 0.2), height: 21.0)) 45 | titleLabel.font = UIFont(name: "Helvetica", size: 18) 46 | titleLabel.backgroundColor = UIColor.clearColor() 47 | titleLabel.textColor = UIColor.blackColor() 48 | titleLabel.textAlignment = .Left 49 | titleLabel.text = Player.sharedInstance.mediaRenderer == nil ? "No Renderer Selected" : Player.sharedInstance.mediaRenderer?.friendlyName 50 | let barButton = UIBarButtonItem(customView: titleLabel) 51 | self.toolbarItems = [Player.sharedInstance.playPauseButton, Player.sharedInstance.stopButton, barButton] 52 | 53 | self.navigationController?.toolbarHidden = false 54 | } 55 | } 56 | 57 | extension FolderViewController: UITableViewDataSource { 58 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 59 | return _playlist.count 60 | } 61 | 62 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 63 | let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier("DefaultCell") as UITableViewCell! 64 | let item = _playlist[indexPath.row] 65 | cell.textLabel?.text = item.title 66 | cell.accessoryType = item is ContentDirectory1Container ? .DisclosureIndicator : .None 67 | 68 | return cell 69 | } 70 | } 71 | 72 | extension FolderViewController: UITableViewDelegate { 73 | func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 74 | let item = _playlist[indexPath.row] 75 | if let containerItem = item as? ContentDirectory1Container { 76 | let targetViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("FolderViewControllerScene") as! FolderViewController 77 | targetViewController.configure(mediaServer: _mediaServer, title: containerItem.title, contentDirectoryID: containerItem.objectID) 78 | self.navigationController?.pushViewController(targetViewController, animated: true) 79 | } 80 | else { 81 | Player.sharedInstance.startPlayback(_playlist, position: indexPath.row) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Source/SSDP/Discovery Adapter/SSDPExplorerDiscoveryAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SSDPExplorerDiscoveryAdapter.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | 26 | class SSDPExplorerDiscoveryAdapter: AbstractSSDPDiscoveryAdapter { 27 | lazy private var _ssdpExplorer = SSDPExplorer() 28 | /// Never reading without writing so a serial queue is adequate 29 | private let _serialSSDPDiscoveryQueue = dispatch_queue_create("com.upnatom.ssdp-explorer-discovery-adapter.ssdp-discovery-queue", DISPATCH_QUEUE_SERIAL) 30 | /// Must be accessed and updated within dispatch_sync() or dispatch_async() to the serial queue 31 | private var _ssdpDiscoveries = [UniqueServiceName: SSDPDiscovery]() 32 | 33 | required init() { 34 | super.init() 35 | 36 | _ssdpExplorer.delegate = self 37 | } 38 | 39 | override func start() { 40 | super.start() 41 | 42 | var types = [SSDPType]() // TODO: Should ideally be a Set, see Github issue #13 43 | for rawSSDPType in rawSSDPTypes { 44 | if let ssdpType = SSDPType(rawValue: rawSSDPType) { 45 | types.append(ssdpType) 46 | } 47 | } 48 | if let resultError = _ssdpExplorer.startExploring(forTypes: types).error { 49 | failed🔰() 50 | notifyDelegate(ofFailure: resultError) 51 | } 52 | } 53 | 54 | override func stop() { 55 | _ssdpExplorer.stopExploring() 56 | 57 | dispatch_async(_serialSSDPDiscoveryQueue, { () -> Void in 58 | self._ssdpDiscoveries.removeAll(keepCapacity: false) 59 | 60 | self.notifyDelegate(ofDiscoveries: Array(self._ssdpDiscoveries.values)) 61 | }) 62 | 63 | super.stop() 64 | } 65 | 66 | private func notifyDelegate(ofFailure error: NSError) { 67 | dispatch_async(delegateQueue, { () -> Void in 68 | self.delegate?.ssdpDiscoveryAdapter(self, didFailWithError: error) 69 | }) 70 | } 71 | 72 | private func notifyDelegate(ofDiscoveries discoveries: [SSDPDiscovery]) { 73 | dispatch_async(delegateQueue, { () -> Void in 74 | self.delegate?.ssdpDiscoveryAdapter(self, didUpdateSSDPDiscoveries: discoveries) 75 | }) 76 | } 77 | } 78 | 79 | extension SSDPExplorerDiscoveryAdapter: SSDPExplorerDelegate { 80 | func ssdpExplorer(explorer: SSDPExplorer, didMakeDiscovery discovery: SSDPDiscovery) { 81 | dispatch_async(_serialSSDPDiscoveryQueue, { () -> Void in 82 | self._ssdpDiscoveries[discovery.usn] = discovery 83 | 84 | self.notifyDelegate(ofDiscoveries: Array(self._ssdpDiscoveries.values)) 85 | }) 86 | } 87 | 88 | func ssdpExplorer(explorer: SSDPExplorer, didRemoveDiscovery discovery: SSDPDiscovery) { 89 | dispatch_async(_serialSSDPDiscoveryQueue, { () -> Void in 90 | if let discovery = self._ssdpDiscoveries[discovery.usn] { 91 | self._ssdpDiscoveries.removeValueForKey(discovery.usn) 92 | 93 | self.notifyDelegate(ofDiscoveries: Array(self._ssdpDiscoveries.values)) 94 | } 95 | }) 96 | } 97 | 98 | func ssdpExplorer(explorer: SSDPExplorer, didFailWithError error: NSError) { 99 | failed🔰() 100 | notifyDelegate(ofFailure: error) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Source/UPnP Foundation/UniqueServiceName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniqueServiceName.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | 26 | /// TODO: For now rooting to NSObject to expose to Objective-C, see Github issue #16 27 | public class UniqueServiceName: NSObject, RawRepresentable { 28 | public let rawValue: RawValue 29 | public let uuid: String 30 | public let urn: String? 31 | public let rootDevice: Bool 32 | 33 | public typealias RawValue = String 34 | 35 | public required init?(rawValue: RawValue) { 36 | self.rawValue = rawValue 37 | 38 | // all forms of usn should contain a uuid, otherwise it's invalid and nil will be returned 39 | guard let uuid = UniqueServiceName.uuid(usn: rawValue) where !uuid.isEmpty else { 40 | /// TODO: Remove default initializations to simply return nil, see Github issue #11 41 | self.uuid = "" 42 | self.rootDevice = false 43 | self.urn = nil 44 | super.init() 45 | return nil 46 | } 47 | 48 | self.uuid = uuid 49 | 50 | rootDevice = UniqueServiceName.isRootDevice(usn: rawValue) 51 | urn = UniqueServiceName.urn(usn: rawValue) 52 | super.init() 53 | } 54 | 55 | convenience public init?(uuid: String, urn: String) { 56 | self.init(rawValue: "\(uuid)::\(urn)") 57 | } 58 | 59 | convenience public init?(uuid: String, rootDevice: Bool) { 60 | let rawValue = rootDevice ? "\(uuid)::upnp:rootdevice" : "\(uuid)" 61 | self.init(rawValue: rawValue) 62 | } 63 | 64 | class func uuid(usn usn: String) -> String? { 65 | let usnComponents = usn.componentsSeparatedByString("::") 66 | return (usnComponents.count >= 1 && usnComponents[0].rangeOfString("uuid:") != nil) ? usnComponents[0] : nil 67 | } 68 | 69 | class func urn(usn usn: String) -> String? { 70 | let usnComponents = usn.componentsSeparatedByString("::") 71 | return (usnComponents.count >= 2 && usnComponents[1].rangeOfString("urn:") != nil) ? usnComponents[1] : nil 72 | } 73 | 74 | class func isRootDevice(usn usn: String) -> Bool { 75 | let usnComponents = usn.componentsSeparatedByString("::") 76 | return usnComponents.count >= 2 && usnComponents[1].rangeOfString("upnp:rootdevice") != nil 77 | } 78 | } 79 | 80 | extension UniqueServiceName { 81 | override public var description: String { 82 | return rawValue 83 | } 84 | } 85 | 86 | extension UniqueServiceName { 87 | override public var hashValue: Int { 88 | return rawValue.hashValue 89 | } 90 | 91 | /// Because self is rooted to NSObject, for now, usage as a key in a dictionary will be treated as a key within an NSDictionary; which requires the overriding the methods hash and isEqual, see Github issue #16 92 | override public var hash: Int { 93 | return hashValue 94 | } 95 | 96 | /// Because self is rooted to NSObject, for now, usage as a key in a dictionary will be treated as a key within an NSDictionary; which requires the overriding the methods hash and isEqual, see Github issue #16 97 | override public func isEqual(object: AnyObject?) -> Bool { 98 | if let other = object as? UniqueServiceName { 99 | return self == other 100 | } 101 | return false 102 | } 103 | } 104 | 105 | public func ==(lhs: UniqueServiceName, rhs: UniqueServiceName) -> Bool { 106 | return lhs.rawValue == rhs.rawValue 107 | } 108 | -------------------------------------------------------------------------------- /Source/AV Profile/Events/AVTransport1Event.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVTransport1Event.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Ono 25 | 26 | public class AVTransport1Event: UPnPEvent { 27 | public var instanceState = [String: AnyObject]() 28 | 29 | override public init(eventXML: NSData, service: AbstractUPnPService) { 30 | super.init(eventXML: eventXML, service: service) 31 | 32 | if let parsedInstanceState = AVTransport1EventParser().parse(eventXML: eventXML).value { 33 | instanceState = parsedInstanceState 34 | } 35 | } 36 | } 37 | 38 | /// for objective-c type checking 39 | extension UPnPEvent { 40 | public func isAVTransport1Event() -> Bool { 41 | return self is AVTransport1Event 42 | } 43 | } 44 | 45 | class AVTransport1EventParser: AbstractDOMXMLParser { 46 | private var _instanceState = [String: AnyObject]() 47 | 48 | override func parse(document document: ONOXMLDocument) -> EmptyResult { 49 | let result: EmptyResult = .Success 50 | 51 | // procedural vs series of nested if let's 52 | guard let lastChangeXMLString = document.firstChildWithXPath("/e:propertyset/e:property/LastChange")?.stringValue() else { 53 | return .Failure(createError("No LastChange element in UPnP service event XML")) 54 | } 55 | 56 | LogVerbose("Parsing LastChange XML:\nSTART\n\(lastChangeXMLString)\nEND") 57 | 58 | guard let lastChangeEventDocument = try? ONOXMLDocument(string: lastChangeXMLString, encoding: NSUTF8StringEncoding) else { 59 | return .Failure(createError("Unable to parse LastChange XML")) 60 | } 61 | 62 | lastChangeEventDocument.definePrefix("avt", forDefaultNamespace: "urn:schemas-upnp-org:metadata-1-0/AVT/") 63 | lastChangeEventDocument.enumerateElementsWithXPath("/avt:Event/avt:InstanceID/*", usingBlock: { [unowned self] (element: ONOXMLElement!, index: UInt, stop: UnsafeMutablePointer) -> Void in 64 | if let stateValue = element.valueForAttribute("val") as? String where !stateValue.isEmpty { 65 | if element.tag.rangeOfString("MetaData") != nil { 66 | guard let metadataDocument = try? ONOXMLDocument(string: stateValue, encoding: NSUTF8StringEncoding) else { 67 | return 68 | } 69 | 70 | LogVerbose("Parsing MetaData XML:\nSTART\n\(stateValue)\nEND") 71 | 72 | var metaData = [String: String]() 73 | 74 | metadataDocument.definePrefix("didllite", forDefaultNamespace: "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/") 75 | metadataDocument.enumerateElementsWithXPath("/didllite:DIDL-Lite/didllite:item/*", usingBlock: { (metadataElement: ONOXMLElement!, index: UInt, stop: UnsafeMutablePointer) -> Void in 76 | if let elementStringValue = metadataElement.stringValue() where !elementStringValue.isEmpty { 77 | metaData[metadataElement.tag] = elementStringValue 78 | } 79 | }) 80 | 81 | self._instanceState[element.tag] = metaData 82 | } else { 83 | self._instanceState[element.tag] = stateValue 84 | } 85 | } 86 | }) 87 | 88 | return result 89 | } 90 | 91 | func parse(eventXML eventXML: NSData) -> Result<[String: AnyObject]> { 92 | switch super.parse(data: eventXML) { 93 | case .Success: 94 | return .Success(_instanceState) 95 | case .Failure(let error): 96 | return .Failure(error) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Source/CocoaSSDP Support/CocoaSSDPDiscoveryAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CocoaSSDPDiscoveryAdapter.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import CocoaSSDP 25 | 26 | class CocoaSSDPDiscoveryAdapter: AbstractSSDPDiscoveryAdapter { 27 | /// Must be set before calling start() 28 | lazy private var _ssdpBrowsers: Set = { 29 | var ssdpBrowsers = Set() 30 | for type in self.rawSSDPTypes { 31 | let ssdpBrowser = SSDPServiceBrowser(serviceType: type) 32 | ssdpBrowser.delegate = self 33 | ssdpBrowsers.insert(ssdpBrowser) 34 | } 35 | 36 | return ssdpBrowsers 37 | }() 38 | /// Never reading without writing so a serial queue is adequate 39 | private let _serialSSDPDiscoveryQueue = dispatch_queue_create("com.upnatom.cocoa-ssdp-discovery-adapter.ssdp-discovery-queue", DISPATCH_QUEUE_SERIAL) 40 | /// Must be accessed and updated within dispatch_sync() or dispatch_async() to the serial queue 41 | private var _ssdpDiscoveries = [String: SSDPDiscovery]() 42 | 43 | override func start() { 44 | super.start() 45 | 46 | for ssdpBrowser in _ssdpBrowsers { 47 | ssdpBrowser.startBrowsingForServices() 48 | } 49 | } 50 | 51 | override func stop() { 52 | for ssdpBrowser in _ssdpBrowsers { 53 | ssdpBrowser.stopBrowsingForServices() 54 | } 55 | 56 | dispatch_async(_serialSSDPDiscoveryQueue, { () -> Void in 57 | self._ssdpDiscoveries.removeAll(keepCapacity: false) 58 | 59 | self.notifyDelegate(ofDiscoveries: self._ssdpDiscoveries.values.array) 60 | }) 61 | 62 | super.stop() 63 | } 64 | 65 | private func notifyDelegate(ofFailure error: NSError) { 66 | dispatch_async(delegateQueue, { () -> Void in 67 | delegate?.ssdpDiscoveryAdapter(self, didFailWithError: error) 68 | }) 69 | } 70 | 71 | private func notifyDelegate(ofDiscoveries discoveries: [SSDPDiscovery]) { 72 | dispatch_async(delegateQueue, { () -> Void in 73 | self.delegate?.ssdpDiscoveryAdapter(self, didUpdateSSDPDiscoveries: discoveries) 74 | }) 75 | } 76 | } 77 | 78 | extension CocoaSSDPDiscoveryAdapter: SSDPServiceBrowserDelegate { 79 | @objc func ssdpBrowser(browser: SSDPServiceBrowser!, didNotStartBrowsingForServices error: NSError!) { 80 | failed🔰() 81 | delegate?.ssdpDiscoveryAdapter(self, didFailWithError: error) 82 | } 83 | 84 | @objc func ssdpBrowser(browser: SSDPServiceBrowser!, didFindService ssdpDiscoveryUnadapted: SSDPService!) { 85 | dispatch_async(_serialSSDPDiscoveryQueue, { () -> Void in 86 | if self._ssdpDiscoveries[ssdpDiscoveryUnadapted.uniqueServiceName] == nil && 87 | returnIfContainsElements(ssdpDiscoveryUnadapted.uniqueServiceName) != nil && 88 | ssdpDiscoveryUnadapted.location != nil && 89 | returnIfContainsElements(ssdpDiscoveryUnadapted.serviceType) != nil { 90 | if let usn = UniqueServiceName(rawValue: ssdpDiscoveryUnadapted.uniqueServiceName), 91 | descriptionURL = ssdpDiscoveryUnadapted.location, 92 | ssdpType = SSDPType(rawValue: ssdpDiscoveryUnadapted.serviceType) { 93 | let ssdpDiscovery = SSDPDiscovery(usn: usn, descriptionURL: descriptionURL, type: ssdpType) 94 | 95 | self._ssdpDiscoveries[ssdpDiscoveryUnadapted.uniqueServiceName] = ssdpDiscovery 96 | 97 | self.notifyDelegate(ofDiscoveries: self._ssdpDiscoveries.values.array) 98 | } 99 | } 100 | }) 101 | } 102 | 103 | @objc func ssdpBrowser(browser: SSDPServiceBrowser!, didRemoveService ssdpDiscoveryUnadapted: SSDPService!) { 104 | dispatch_async(_serialSSDPDiscoveryQueue, { () -> Void in 105 | if self._ssdpDiscoveries[ssdpDiscoveryUnadapted.uniqueServiceName] != nil { 106 | self._ssdpDiscoveries.removeValueForKey(ssdpDiscoveryUnadapted.uniqueServiceName) 107 | 108 | self.notifyDelegate(ofDiscoveries: self._ssdpDiscoveries.values.array) 109 | } 110 | }) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/FolderViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // FolderViewController.m 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | #import "FolderViewController.h" 25 | #import "Player.h" 26 | @import UPnAtom; 27 | 28 | @interface FolderViewController () 29 | @property (nonatomic) IBOutlet UITableView *tableView; 30 | @end 31 | 32 | @implementation FolderViewController { 33 | NSString *_rootId; 34 | MediaServer1Device *_device; 35 | NSArray *_playlist; 36 | } 37 | 38 | - (void)viewDidLoad { 39 | [super viewDidLoad]; 40 | 41 | [_device.contentDirectoryService getSortCapabilities:^(NSString *sortCaps) { 42 | if ([sortCaps rangeOfString:@"dc:title"].location != NSNotFound) { 43 | sortCaps = @"+dc:title"; 44 | } 45 | 46 | [_device.contentDirectoryService browseWithObjectID:_rootId browseFlag:@"BrowseDirectChildren" filter:@"*" startingIndex:@"0" requestedCount:@"0" sortCriteria:sortCaps success:^(NSArray *result, NSInteger numberReturned, NSInteger totalMatches, NSString *updateID) { 47 | _playlist = result; 48 | [self.tableView reloadData]; 49 | } failure:^(NSError *error) { 50 | NSLog(@"failed to browse content directory"); 51 | }]; 52 | } failure:^(NSError *error) { 53 | NSLog(@"failed to get sort capabilities"); 54 | }]; 55 | 56 | CGFloat viewWidth = self.navigationController.view.frame.size.width; 57 | UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0 , 11.0f, viewWidth - (viewWidth * 0.2), 21.0f)]; 58 | [titleLabel setFont:[UIFont fontWithName:@"Helvetica" size:18]]; 59 | [titleLabel setBackgroundColor:[UIColor clearColor]]; 60 | [titleLabel setTextColor:[UIColor blackColor]]; 61 | 62 | if([[Player sharedInstance] mediaRenderer] == nil){ 63 | [titleLabel setText:@"No Renderer Selected"]; 64 | }else{ 65 | [titleLabel setText:[[[Player sharedInstance] mediaRenderer] friendlyName] ]; 66 | } 67 | 68 | [titleLabel setTextAlignment:NSTextAlignmentLeft]; 69 | UIBarButtonItem *title = [[UIBarButtonItem alloc] initWithCustomView:titleLabel]; 70 | NSArray *items = @[[[Player sharedInstance] playPauseButton], [[Player sharedInstance] stopButton], title]; 71 | self.toolbarItems = items; 72 | 73 | self.navigationController.toolbarHidden = NO; 74 | } 75 | 76 | - (void)configureWithDevice:(MediaServer1Device *)device header:(NSString *)header rootId:(NSString *)rootId{ 77 | _device = device; 78 | _rootId = rootId; 79 | self.title = header; 80 | _playlist = [NSArray array]; 81 | } 82 | 83 | #pragma mark - UITableViewDataSource methods 84 | 85 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 86 | return [_playlist count]; 87 | } 88 | 89 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 90 | static NSString *cellIdentifier = @"DefaultCell"; 91 | 92 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; 93 | 94 | // Configure the cell... 95 | ContentDirectory1Object *item = _playlist[indexPath.row]; 96 | [[cell textLabel] setText:[item title]]; 97 | 98 | cell.accessoryType = item.isContentDirectory1Container ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone; 99 | 100 | return cell; 101 | } 102 | 103 | #pragma mark - UITableViewDelegate methods 104 | 105 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 106 | ContentDirectory1Object *item = _playlist[indexPath.row]; 107 | if([item isContentDirectory1Container]){ 108 | ContentDirectory1Container *container = _playlist[indexPath.row]; 109 | UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil]; 110 | FolderViewController *targetViewController = [storyboard instantiateViewControllerWithIdentifier:@"FolderViewControllerScene"]; 111 | [targetViewController configureWithDevice:_device header:[container title] rootId:[container objectID]]; 112 | 113 | [[self navigationController] pushViewController:targetViewController animated:YES]; 114 | }else{ 115 | [[Player sharedInstance] startPlayback:_playlist position:indexPath.row]; 116 | } 117 | } 118 | 119 | @end 120 | -------------------------------------------------------------------------------- /Source/HTTP Client Session Managers and Serializers/SOAPSerialization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SOAPSerialization.swift 3 | // ControlPointDemo 4 | // 5 | // SOAPSerialization.swift 6 | // 7 | // Copyright (c) 2015 David Robles 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in all 17 | // copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | // SOFTWARE. 26 | 27 | import Foundation 28 | import AFNetworking 29 | import Ono 30 | 31 | public class SOAPRequestSerializer: AFHTTPRequestSerializer { 32 | public class Parameters { 33 | let soapAction: String 34 | let serviceURN: String 35 | let arguments: NSDictionary? 36 | 37 | public init(soapAction: String, serviceURN: String, arguments: NSDictionary?) { 38 | self.soapAction = soapAction 39 | self.serviceURN = serviceURN 40 | self.arguments = arguments 41 | } 42 | } 43 | 44 | override public func requestBySerializingRequest(request: NSURLRequest, withParameters parameters: AnyObject?, error: NSErrorPointer) -> NSURLRequest? { 45 | guard let requestParameters = parameters as? Parameters else { 46 | return nil 47 | } 48 | 49 | let mutableRequest: NSMutableURLRequest = request.mutableCopy() as! NSMutableURLRequest 50 | 51 | for (field, value) in self.HTTPRequestHeaders { 52 | if let field = field as? String, value = value as? String where request.valueForHTTPHeaderField(field) == nil { 53 | mutableRequest.setValue(value, forHTTPHeaderField: field) 54 | } 55 | } 56 | 57 | if mutableRequest.valueForHTTPHeaderField("Content-Type") == nil { 58 | let charSet = CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding)) 59 | mutableRequest.setValue("text/xml; charset=\"\(charSet)\"", forHTTPHeaderField: "Content-Type") 60 | } 61 | 62 | mutableRequest.setValue("\"\(requestParameters.serviceURN)#\(requestParameters.soapAction)\"", forHTTPHeaderField: "SOAPACTION") 63 | 64 | var body = "" 65 | body += "" 66 | body += "" 67 | body += "" 68 | if let arguments = requestParameters.arguments { 69 | for (key, value) in arguments { 70 | body += "<\(key)>\(value)" 71 | } 72 | } 73 | body += "" 74 | body += "" 75 | LogVerbose("SOAP request body: \(body)") 76 | 77 | mutableRequest.setValue("\(body.utf8.count)", forHTTPHeaderField: "Content-Length") 78 | 79 | mutableRequest.HTTPBody = body.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false) 80 | 81 | return mutableRequest 82 | } 83 | } 84 | 85 | public class SOAPResponseSerializer: AFXMLParserResponseSerializer { 86 | override public func responseObjectForResponse(response: NSURLResponse?, data: NSData?, error: NSErrorPointer) -> AnyObject? { 87 | do { 88 | try validateResponse(response as! NSHTTPURLResponse, data: data) 89 | } catch { 90 | return nil 91 | } 92 | let xmlParser = SOAPResponseParser() 93 | 94 | guard let data = data else { 95 | return nil 96 | } 97 | 98 | switch xmlParser.parse(soapResponseData: data) { 99 | case .Success(let value): 100 | return value 101 | case .Failure(let error): 102 | return nil 103 | } 104 | } 105 | } 106 | 107 | class SOAPResponseParser: AbstractDOMXMLParser { 108 | private var _responseParameters = [String: String]() 109 | 110 | override func parse(document document: ONOXMLDocument) -> EmptyResult { 111 | var result: EmptyResult = .Success 112 | document.enumerateElementsWithXPath("/s:Envelope/s:Body/*/*", usingBlock: { (element: ONOXMLElement!, index: UInt, stop: UnsafeMutablePointer) -> Void in 113 | if let elementTag = element.tag, elementValue = element.stringValue() where 114 | elementTag.characters.count > 0 && elementValue.characters.count > 0 && elementValue != "NOT_IMPLEMENTED" { 115 | self._responseParameters[elementTag] = elementValue 116 | } 117 | 118 | result = .Success 119 | }) 120 | 121 | LogVerbose("SOAP response values: \(prettyPrint(_responseParameters))") 122 | 123 | return result 124 | } 125 | 126 | func parse(soapResponseData soapResponseData: NSData) -> Result<[String: String]> { 127 | switch super.parse(data: soapResponseData) { 128 | case .Success: 129 | return .Success(_responseParameters) 130 | case .Failure(let error): 131 | return .Failure(error) 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Source/Logging/PropertyPrinter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PropertyPrinter.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | 26 | public protocol ExtendedPrintable: CustomStringConvertible { 27 | var className: String { get } 28 | } 29 | 30 | public struct PropertyPrinter { 31 | private var _properties = [String: String]() 32 | 33 | public init() { } 34 | 35 | public mutating func add(propertyName: String, property: T?) { 36 | _properties[propertyName] = prettyPrint(property) 37 | } 38 | 39 | public mutating func add(propertyName: String, property: T?) { 40 | _properties[propertyName] = prettyPrint(property) 41 | } 42 | 43 | public mutating func add(propertyName: String, property: [T]?) { 44 | _properties[propertyName] = prettyPrint(property) 45 | } 46 | 47 | public mutating func add(propertyName: String, property: [T]?) { 48 | _properties[propertyName] = prettyPrint(property) 49 | } 50 | 51 | public mutating func add(propertyName: String, property: [K: V]?) { 52 | _properties[propertyName] = prettyPrint(property) 53 | } 54 | 55 | public mutating func add(propertyName: String, property: [K: V]?) { 56 | _properties[propertyName] = prettyPrint(property) 57 | } 58 | 59 | // subscript(key: String) -> Any? { // subscripts can't be used as the subscript function can't be Generic only class/struct-wide generic declarations can be made which isn't useful here 60 | // get { 61 | // return nil 62 | // } 63 | // set { 64 | // add(key, property: newValue) 65 | // } 66 | // } 67 | } 68 | 69 | extension PropertyPrinter: CustomStringConvertible { 70 | public var description: String { 71 | return dictionaryDescription(_properties, pointsToSymbol: "=") 72 | } 73 | } 74 | 75 | /// helpPrint(), dictionaryDescription(), and arrayDescription() have so many variants due to the limitation in the Swift for detecting swift-protocol conformance at runtime. This restriction will be removed in a future release of swift: http://stackoverflow.com/questions/26909974/protocols-why-is-objc-required-for-conformance-checking-and-optional-requireme 76 | func prettyPrint(someDictionary: [K: V]?) -> String { 77 | if let someDictionary = someDictionary { 78 | return prettyPrint(dictionaryDescription(someDictionary, pointsToSymbol: ":")) 79 | } 80 | 81 | return "nil" 82 | } 83 | 84 | func prettyPrint(someDictionary: [K: V]?) -> String { 85 | if let someDictionary = someDictionary { 86 | return prettyPrint(dictionaryDescription(someDictionary, pointsToSymbol: ":")) 87 | } 88 | 89 | return "nil" 90 | } 91 | 92 | func prettyPrint(someArray: [T]?) -> String { 93 | if let someArray = someArray { 94 | return prettyPrint(arrayDescription(someArray)) 95 | } 96 | 97 | return "nil" 98 | } 99 | 100 | func prettyPrint(someArray: [T]?) -> String { 101 | if let someArray = someArray { 102 | return prettyPrint(arrayDescription(someArray)) 103 | } 104 | 105 | return "nil" 106 | } 107 | 108 | func prettyPrint(something: T?) -> String { 109 | if let something = something { 110 | return something.description.stringByReplacingOccurrencesOfString("\n", withString: "\n\t", options: .LiteralSearch) 111 | } 112 | 113 | return "nil" 114 | } 115 | 116 | func prettyPrint(something: T?) -> String { 117 | if let something = something { 118 | return "\(something)".stringByReplacingOccurrencesOfString("\n", withString: "\n\t", options: .LiteralSearch) 119 | } 120 | 121 | return "nil" 122 | } 123 | 124 | func dictionaryDescription(properties: [K: V], pointsToSymbol: String) -> String { 125 | var description = "{ \n" 126 | for (key, value) in properties { 127 | let valueDescription = value.description.stringByReplacingOccurrencesOfString("\n", withString: "\n\t", options: .LiteralSearch) 128 | description += "\t\(key) \(pointsToSymbol) \(valueDescription) \n" 129 | } 130 | description += "}" 131 | return description 132 | } 133 | 134 | func dictionaryDescription(properties: [K: V], pointsToSymbol: String) -> String { 135 | var description = "{ \n" 136 | for (key, value) in properties { 137 | description += "\t\(key) \(pointsToSymbol) \(value) \n" 138 | } 139 | description += "}" 140 | return description 141 | } 142 | 143 | func arrayDescription(array: [T]) -> String { 144 | var description = "{ \n" 145 | for item in array { 146 | let itemDescription = item.description.stringByReplacingOccurrencesOfString("\n", withString: "\n\t", options: .LiteralSearch) 147 | description += "\t\(itemDescription)\n" 148 | } 149 | description += "}" 150 | return description 151 | } 152 | 153 | func arrayDescription(array: [T]) -> String { 154 | var description = "{ \n" 155 | for item in array { 156 | description += "\t\(item)\n" 157 | } 158 | description += "}" 159 | return description 160 | } 161 | -------------------------------------------------------------------------------- /Source/AV Profile/Services/ConnectionManager1Service.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionManager1Service.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | 26 | public class ConnectionManager1Service: AbstractUPnPService { 27 | public func getProtocolInfo(success: (source: [String], sink: [String]) -> Void, failure: (error: NSError) -> Void) { 28 | let parameters = SOAPRequestSerializer.Parameters(soapAction: "GetProtocolInfo", serviceURN: urn, arguments: nil) 29 | 30 | soapSessionManager.POST(self.controlURL.absoluteString, parameters: parameters, success: { (task: NSURLSessionDataTask, responseObject: AnyObject?) -> Void in 31 | let responseObject = responseObject as? [String: String] 32 | success(source: responseObject?["Source"]?.componentsSeparatedByString(",") ?? [String](), sink: responseObject?["Sink"]?.componentsSeparatedByString(",") ?? [String]()) 33 | }, failure: { (task: NSURLSessionDataTask?, error: NSError) -> Void in 34 | failure(error: error) 35 | }) 36 | } 37 | 38 | public func prepareForConnection(remoteProtocolInfo remoteProtocolInfo: String, peerConnectionManager: String, peerConnectionID: String, direction: String, success: (connectionID: String?, avTransportID: String?, renderingControlServiceID: String?) -> Void, failure:(error: NSError) -> Void) { 39 | let arguments = [ 40 | "RemoteProtocolInfo" : remoteProtocolInfo, 41 | "PeerConnectionManager" : peerConnectionManager, 42 | "PeerConnectionID" : peerConnectionID, 43 | "Direction" : direction] 44 | 45 | let parameters = SOAPRequestSerializer.Parameters(soapAction: "PrepareForConnection", serviceURN: urn, arguments: arguments) 46 | 47 | // Check if the optional SOAP action "PrepareForConnection" is supported 48 | supportsSOAPAction(actionParameters: parameters) { (isSupported) -> Void in 49 | if isSupported { 50 | self.soapSessionManager.POST(self.controlURL.absoluteString, parameters: parameters, success: { (task: NSURLSessionDataTask, responseObject: AnyObject?) -> Void in 51 | let responseObject = responseObject as? [String: String] 52 | success(connectionID: responseObject?["ConnectionID"], avTransportID: responseObject?["AVTransportID"], renderingControlServiceID: responseObject?["RcsID"]) 53 | }, failure: { (task: NSURLSessionDataTask?, error: NSError) -> Void in 54 | failure(error: error) 55 | }) 56 | } else { 57 | failure(error: createError("SOAP action '\(parameters.soapAction)' unsupported by service \(self.urn) on device \(self.device?.friendlyName)")) 58 | } 59 | } 60 | } 61 | 62 | public func connectionComplete(connectionID connectionID: String, success: () -> Void, failure:(error: NSError) -> Void) { 63 | let arguments = ["ConnectionID" : connectionID] 64 | 65 | let parameters = SOAPRequestSerializer.Parameters(soapAction: "ConnectionComplete", serviceURN: urn, arguments: arguments) 66 | 67 | // Check if the optional SOAP action "ConnectionComplete" is supported 68 | supportsSOAPAction(actionParameters: parameters) { (isSupported) -> Void in 69 | if isSupported { 70 | self.soapSessionManager.POST(self.controlURL.absoluteString, parameters: parameters, success: { (task: NSURLSessionDataTask, responseObject: AnyObject?) -> Void in 71 | success() 72 | }, failure: { (task: NSURLSessionDataTask?, error: NSError) -> Void in 73 | failure(error: error) 74 | }) 75 | } else { 76 | failure(error: createError("SOAP action '\(parameters.soapAction)' unsupported by service \(self.urn) on device \(self.device?.friendlyName)")) 77 | } 78 | } 79 | } 80 | 81 | public func getCurrentConnectionIDs(success: (connectionIDs: [String]) -> Void, failure: (error: NSError) -> Void) { 82 | let parameters = SOAPRequestSerializer.Parameters(soapAction: "GetCurrentConnectionIDs", serviceURN: urn, arguments: nil) 83 | 84 | soapSessionManager.POST(self.controlURL.absoluteString, parameters: parameters, success: { (task: NSURLSessionDataTask, responseObject: AnyObject?) -> Void in 85 | let responseObject = responseObject as? [String: String] 86 | success(connectionIDs: responseObject?["ConnectionIDs"]?.componentsSeparatedByString(",") ?? [String]()) 87 | }, failure: { (task: NSURLSessionDataTask?, error: NSError) -> Void in 88 | failure(error: error) 89 | }) 90 | } 91 | 92 | public func getCurrentConnectionInfo(connectionID connectionID: String, success: (renderingControlServiceID: String?, avTransportID: String?, protocolInfo: String?, peerConnectionManager: String?, peerConnectionID: String?, direction: String?, status: String?) -> Void, failure: (error: NSError) -> Void) { 93 | let arguments = ["ConnectionID" : connectionID] 94 | 95 | let parameters = SOAPRequestSerializer.Parameters(soapAction: "GetCurrentConnectionInfo", serviceURN: urn, arguments: arguments) 96 | 97 | soapSessionManager.POST(self.controlURL.absoluteString, parameters: parameters, success: { (task: NSURLSessionDataTask, responseObject: AnyObject?) -> Void in 98 | let responseObject = responseObject as? [String: String] 99 | success(renderingControlServiceID: responseObject?["RcsID"], avTransportID: responseObject?["AVTransportID"], protocolInfo: responseObject?["ProtocolInfo"], peerConnectionManager: responseObject?["PeerConnectionManager"], peerConnectionID: responseObject?["PeerConnectionID"], direction: responseObject?["Direction"], status: responseObject?["Status"]) 100 | }, failure: { (task: NSURLSessionDataTask?, error: NSError) -> Void in 101 | failure(error: error) 102 | }) 103 | } 104 | } 105 | 106 | /// for objective-c type checking 107 | extension AbstractUPnP { 108 | public func isConnectionManager1Service() -> Bool { 109 | return self is ConnectionManager1Service 110 | } 111 | } 112 | 113 | /// overrides ExtendedPrintable protocol implementation 114 | extension ConnectionManager1Service { 115 | override public var className: String { return "\(self.dynamicType)" } 116 | override public var description: String { 117 | var properties = PropertyPrinter() 118 | properties.add(super.className, property: super.description) 119 | return properties.description 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_Swift/ControlPointDemo/Player.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Player.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import UIKit 25 | import UPnAtom 26 | 27 | private let _PlayerSharedInstance = Player() 28 | 29 | class Player { 30 | class var sharedInstance: Player { 31 | return _PlayerSharedInstance 32 | } 33 | var mediaServer: MediaServer1Device? 34 | var mediaRenderer: MediaRenderer1Device? { 35 | didSet { 36 | didSetRenderer(oldRenderer: oldValue, newRenderer: mediaRenderer) 37 | } 38 | } 39 | private(set) var playPauseButton: UIBarButtonItem! // TODO: Should ideally be a constant, see Github issue #10 40 | private(set) var stopButton: UIBarButtonItem! // TODO: Should ideally be a constant, see Github issue #10 41 | 42 | private var _position: Int = 0 43 | private var _playlist: [ContentDirectory1Object]? 44 | private var _avTransportEventObserver: AnyObject? 45 | private var _playerState: PlayerState = PlayerState.Stopped { 46 | didSet { 47 | playerStateDidChange() 48 | } 49 | } 50 | private var _avTransportInstanceID = "0" 51 | 52 | enum PlayerState { 53 | case Unknown 54 | case Stopped 55 | case Playing 56 | case Paused 57 | } 58 | 59 | init() { 60 | playPauseButton = UIBarButtonItem(image: UIImage(named: "play_button"), style: .Plain, target: self, action: #selector(Player.playPauseButtonTapped(_:))) 61 | stopButton = UIBarButtonItem(image: UIImage(named: "stop_button"), style: .Plain, target: self, action: #selector(Player.stopButtonTapped(_:))) 62 | } 63 | 64 | func startPlayback(playlist: [ContentDirectory1Object], position: Int) { 65 | _playlist = playlist 66 | 67 | startPlayback(position: position) 68 | } 69 | 70 | func startPlayback(position position: Int) { 71 | _position = position 72 | 73 | if let item = _playlist?[position] as? ContentDirectory1VideoItem { 74 | let uri = item.resourceURL.absoluteString 75 | let instanceID = _avTransportInstanceID 76 | mediaRenderer?.avTransportService?.setAVTransportURI(instanceID: instanceID, currentURI: uri, currentURIMetadata: "", success: { () -> Void in 77 | print("URI set succeeded!") 78 | self.play({ () -> Void in 79 | print("Play command succeeded!") 80 | }, failure: { (error) -> Void in 81 | print("Play command failed: \(error)") 82 | }) 83 | 84 | }, failure: { (error) -> Void in 85 | print("URI set failed: \(error)") 86 | }) 87 | } 88 | } 89 | 90 | @objc private func playPauseButtonTapped(sender: AnyObject) { 91 | print("play/pause button tapped") 92 | 93 | switch _playerState { 94 | case .Playing: 95 | pause({ () -> Void in 96 | print("Pause command succeeded!") 97 | }, failure: { (error) -> Void in 98 | print("Pause command failed: \(error)") 99 | }) 100 | case .Paused, .Stopped: 101 | play({ () -> Void in 102 | print("Play command succeeded!") 103 | }, failure: { (error) -> Void in 104 | print("Play command failed: \(error)") 105 | }) 106 | default: 107 | print("Play/pause button cannot be used in this state.") 108 | } 109 | } 110 | 111 | @objc private func stopButtonTapped(sender: AnyObject) { 112 | print("stop button tapped") 113 | 114 | switch _playerState { 115 | case .Playing, .Paused: 116 | stop({ () -> Void in 117 | print("Stop command succeeded!") 118 | }, failure: { (error) -> Void in 119 | print("Stop command failed: \(error)") 120 | }) 121 | case .Stopped: 122 | print("Stop button cannot be used in this state.") 123 | default: 124 | print("Stop button cannot be used in this state.") 125 | } 126 | } 127 | 128 | private func didSetRenderer(oldRenderer oldRenderer: MediaRenderer1Device?, newRenderer: MediaRenderer1Device?) { 129 | if let avTransportEventObserver: AnyObject = _avTransportEventObserver { 130 | oldRenderer?.avTransportService?.removeEventObserver(avTransportEventObserver) 131 | } 132 | 133 | _avTransportEventObserver = newRenderer?.avTransportService?.addEventObserver(NSOperationQueue.currentQueue(), callBackBlock: { (event: UPnPEvent) -> Void in 134 | if let avTransportEvent = event as? AVTransport1Event, 135 | transportState = (avTransportEvent.instanceState["TransportState"] as? String)?.lowercaseString { 136 | print("\(event.service?.className) Event: \(avTransportEvent.instanceState)") 137 | print("transport state: \(transportState)") 138 | if transportState.rangeOfString("playing") != nil { 139 | self._playerState = .Playing 140 | } 141 | else if transportState.rangeOfString("paused") != nil { 142 | self._playerState = .Paused 143 | } 144 | else if transportState.rangeOfString("stopped") != nil { 145 | self._playerState = .Stopped 146 | } 147 | else { 148 | self._playerState = .Unknown 149 | } 150 | } 151 | }) 152 | } 153 | 154 | private func playerStateDidChange() { 155 | switch _playerState { 156 | case .Stopped, .Paused, .Unknown: 157 | playPauseButton.image = UIImage(named: "play_button") 158 | case .Playing: 159 | playPauseButton.image = UIImage(named: "pause_button") 160 | } 161 | } 162 | 163 | private func play(success: () -> Void, failure:(error: NSError) -> Void) { 164 | self.mediaRenderer?.avTransportService?.play(instanceID: _avTransportInstanceID, speed: "1", success: success, failure: failure) 165 | } 166 | 167 | private func pause(success: () -> Void, failure:(error: NSError) -> Void) { 168 | self.mediaRenderer?.avTransportService?.pause(instanceID: _avTransportInstanceID, success: success, failure: failure) 169 | } 170 | 171 | private func stop(success: () -> Void, failure:(error: NSError) -> Void) { 172 | self.mediaRenderer?.avTransportService?.stop(instanceID: _avTransportInstanceID, success: success, failure: failure) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Source/HTTP Client Session Managers and Serializers/UPnPEventSubscriptionSerialization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UPnPEventSubscriptionSerialization.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | import AFNetworking 26 | 27 | class UPnPEventSubscribeRequestSerializer: AFHTTPRequestSerializer { 28 | class Parameters { 29 | let callBack: NSURL 30 | let timeout: Int // in seconds 31 | 32 | init(callBack: NSURL, timeout: Int) { 33 | self.callBack = callBack 34 | self.timeout = timeout 35 | } 36 | } 37 | 38 | override func requestBySerializingRequest(request: NSURLRequest, withParameters parameters: AnyObject?, error: NSErrorPointer) -> NSURLRequest? { 39 | guard let requestParameters = parameters as? Parameters else { 40 | return nil 41 | } 42 | 43 | let mutableRequest: NSMutableURLRequest = request.mutableCopy() as! NSMutableURLRequest 44 | 45 | for (field, value) in self.HTTPRequestHeaders { 46 | if let field = field as? String, value = value as? String where request.valueForHTTPHeaderField(field) == nil { 47 | mutableRequest.setValue(value, forHTTPHeaderField: field) 48 | } 49 | } 50 | 51 | let callBackString = requestParameters.callBack.absoluteString 52 | mutableRequest.setValue("<\(callBackString)>", forHTTPHeaderField: "CALLBACK") 53 | 54 | mutableRequest.setValue("upnp:event", forHTTPHeaderField: "NT") 55 | mutableRequest.setValue("Second-\(requestParameters.timeout)", forHTTPHeaderField: "TIMEOUT") 56 | 57 | return mutableRequest 58 | } 59 | } 60 | 61 | class UPnPEventSubscribeResponseSerializer: AFHTTPResponseSerializer { 62 | class Response { 63 | let subscriptionID: String 64 | let timeout: Int // in seconds 65 | 66 | init(subscriptionID: String, timeout: Int) { 67 | self.subscriptionID = subscriptionID 68 | self.timeout = timeout 69 | } 70 | } 71 | 72 | override func responseObjectForResponse(response: NSURLResponse?, data: NSData?, error: NSErrorPointer) -> AnyObject? { 73 | do { 74 | try validateResponse(response as! NSHTTPURLResponse, data: data) 75 | } catch { 76 | return nil 77 | } 78 | 79 | if let subscriptionID = (response as! NSHTTPURLResponse).allHeaderFields["SID"] as? String, 80 | timeoutString = (response as! NSHTTPURLResponse).allHeaderFields["TIMEOUT"] as? String, 81 | secondKeywordRange = timeoutString.rangeOfString("Second-"), 82 | timeout = Int(timeoutString.substringWithRange(Range(start: secondKeywordRange.endIndex, end: timeoutString.endIndex))) { 83 | return Response(subscriptionID: subscriptionID, timeout: timeout) 84 | } else { 85 | return nil 86 | } 87 | } 88 | } 89 | 90 | class UPnPEventRenewSubscriptionRequestSerializer: AFHTTPRequestSerializer { 91 | class Parameters { 92 | let subscriptionID: String 93 | let timeout: Int // in seconds 94 | 95 | init(subscriptionID: String, timeout: Int) { 96 | self.subscriptionID = subscriptionID 97 | self.timeout = timeout 98 | } 99 | } 100 | 101 | override func requestBySerializingRequest(request: NSURLRequest, withParameters parameters: AnyObject?, error: NSErrorPointer) -> NSURLRequest? { 102 | guard let requestParameters = parameters as? Parameters else { 103 | return nil 104 | } 105 | 106 | let mutableRequest: NSMutableURLRequest = request.mutableCopy() as! NSMutableURLRequest 107 | 108 | for (field, value) in self.HTTPRequestHeaders { 109 | if let field = field as? String, value = value as? String where request.valueForHTTPHeaderField(field) == nil { 110 | mutableRequest.setValue(value, forHTTPHeaderField: field) 111 | } 112 | } 113 | 114 | mutableRequest.setValue("\(requestParameters.subscriptionID)", forHTTPHeaderField: "SID") 115 | mutableRequest.setValue("Second-\(requestParameters.timeout)", forHTTPHeaderField: "TIMEOUT") 116 | 117 | return mutableRequest 118 | } 119 | } 120 | 121 | class UPnPEventRenewSubscriptionResponseSerializer: AFHTTPResponseSerializer { 122 | class Response { 123 | let subscriptionID: String 124 | let timeout: Int // in seconds 125 | 126 | init(subscriptionID: String, timeout: Int) { 127 | self.subscriptionID = subscriptionID 128 | self.timeout = timeout 129 | } 130 | } 131 | 132 | override func responseObjectForResponse(response: NSURLResponse?, data: NSData?, error: NSErrorPointer) -> AnyObject? { 133 | do { 134 | try validateResponse(response as! NSHTTPURLResponse, data: data) 135 | } catch { 136 | return nil 137 | } 138 | 139 | guard let subscriptionID = (response as! NSHTTPURLResponse).allHeaderFields["SID"] as? String, 140 | timeoutString = (response as! NSHTTPURLResponse).allHeaderFields["TIMEOUT"] as? String, 141 | secondKeywordRange = timeoutString.rangeOfString("Second-"), 142 | timeout = Int(timeoutString.substringWithRange(Range(start: secondKeywordRange.endIndex, end: timeoutString.endIndex))) else { 143 | return nil 144 | } 145 | 146 | return Response(subscriptionID: subscriptionID, timeout: timeout) 147 | } 148 | } 149 | 150 | class UPnPEventUnsubscribeRequestSerializer: AFHTTPRequestSerializer { 151 | class Parameters { 152 | let subscriptionID: String 153 | 154 | init(subscriptionID: String) { 155 | self.subscriptionID = subscriptionID 156 | } 157 | } 158 | 159 | override func requestBySerializingRequest(request: NSURLRequest, withParameters parameters: AnyObject?, error: NSErrorPointer) -> NSURLRequest? { 160 | guard let requestParameters = parameters as? Parameters else { 161 | return nil 162 | } 163 | 164 | let mutableRequest: NSMutableURLRequest = request.mutableCopy() as! NSMutableURLRequest 165 | 166 | for (field, value) in self.HTTPRequestHeaders { 167 | if let field = field as? String, value = value as? String where request.valueForHTTPHeaderField(field) == nil { 168 | mutableRequest.setValue(value, forHTTPHeaderField: field) 169 | } 170 | } 171 | 172 | mutableRequest.setValue("\(requestParameters.subscriptionID)", forHTTPHeaderField: "SID") 173 | 174 | return mutableRequest 175 | } 176 | } 177 | 178 | class UPnPEventUnsubscribeResponseSerializer: AFHTTPResponseSerializer { 179 | override func responseObjectForResponse(response: NSURLResponse?, data: NSData?, error: NSErrorPointer) -> AnyObject? { 180 | do { 181 | try validateResponse(response as! NSHTTPURLResponse, data: data) 182 | } catch { 183 | return nil 184 | } 185 | 186 | return "Success" 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Source/Parsers/AbstractSAXXMLParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AbstractSAXXMLParser.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | 26 | // Subclassing NSObject in order to be a NSXMLParserDelegate 27 | public class AbstractSAXXMLParser: NSObject { 28 | private let _supportNamespaces: Bool 29 | lazy private var _elementStack = [String]() 30 | lazy private var _elementObservations = [SAXXMLParserElementObservation]() 31 | 32 | public init(supportNamespaces: Bool) { 33 | _supportNamespaces = supportNamespaces 34 | } 35 | 36 | convenience override init() { 37 | self.init(supportNamespaces: false) 38 | } 39 | 40 | public func addElementObservation(elementObservation: SAXXMLParserElementObservation) { 41 | _elementObservations.append(elementObservation) 42 | } 43 | 44 | public func clearAllElementObservations() { 45 | _elementObservations.removeAll(keepCapacity: false) 46 | } 47 | 48 | func elementObservationForElementStack(elementStack: [String]) -> SAXXMLParserElementObservation? { 49 | for elementObservation in _elementObservations { 50 | // Full compares go first 51 | guard elementObservation.elementPath != elementStack else { 52 | return elementObservation 53 | } 54 | 55 | // * -> leafX -> leafY 56 | // Maybe we have a wildchar, that means that the path after the wildchar must match 57 | if elementObservation.elementPath.first == "*" && 58 | elementStack.count >= elementObservation.elementPath.count { 59 | var tempElementStack = elementStack 60 | var tempObservationElementPath = elementObservation.elementPath 61 | 62 | // cut the * from our asset path 63 | tempObservationElementPath.removeAtIndex(0) 64 | 65 | // make our (copy of the) curents stack the same length 66 | let elementsToRemove: Int = tempElementStack.count - tempObservationElementPath.count 67 | let range = Range(start: 0, end: elementsToRemove) 68 | tempElementStack.removeRange(range) 69 | if tempObservationElementPath == tempElementStack { 70 | return elementObservation 71 | } 72 | } 73 | 74 | // leafX -> leafY -> * 75 | if elementObservation.elementPath.last == "*" && 76 | elementStack.count == elementObservation.elementPath.count && elementStack.count > 1 { 77 | var tempElementStack = elementStack 78 | var tempObservationElementPath = elementObservation.elementPath 79 | // Cut the last entry (which is * in one array and in the other 80 | tempElementStack.removeLast() 81 | tempObservationElementPath.removeLast() 82 | if tempElementStack == tempObservationElementPath { 83 | return elementObservation 84 | } 85 | } 86 | } 87 | 88 | return nil 89 | } 90 | 91 | public func parse(data data: NSData) -> EmptyResult { 92 | var parserResult: EmptyResult = .Failure(createError("Parser failure")) 93 | autoreleasepool { () -> () in 94 | if let validData = self.validateForParsing(data) { 95 | let parser = NSXMLParser(data: validData) 96 | parserResult = self.startParser(parser) 97 | } 98 | } 99 | 100 | return parserResult 101 | } 102 | 103 | // MARK: - Internal lib 104 | 105 | private func startParser(parser: NSXMLParser) -> EmptyResult { 106 | parser.shouldProcessNamespaces = _supportNamespaces 107 | parser.delegate = self 108 | 109 | var parserResult: EmptyResult = .Failure(createError("Parser failure")) 110 | if parser.parse() { 111 | parserResult = .Success 112 | } else { 113 | if let parserError = parser.parserError { 114 | parserResult = .Failure(parserError) 115 | } 116 | } 117 | 118 | parser.delegate = nil 119 | 120 | return parserResult 121 | } 122 | 123 | private func validateForParsing(data: NSData) -> NSData? { 124 | guard let xmlStringOptional = NSString(data: data, encoding: NSUTF8StringEncoding), 125 | let regexOptional = try? NSRegularExpression(pattern: "^\\s*$\\r?\\n", options: .AnchorsMatchLines) else { 126 | return nil 127 | } 128 | 129 | let validXMLString = regexOptional.stringByReplacingMatchesInString(xmlStringOptional as String, options: NSMatchingOptions(rawValue: 0), range: NSMakeRange(0, xmlStringOptional.length), withTemplate: "") 130 | return validXMLString.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true) 131 | } 132 | } 133 | 134 | extension AbstractSAXXMLParser: NSXMLParserDelegate { 135 | public func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) { 136 | _elementStack += [elementName] 137 | 138 | if let elementObservation = elementObservationForElementStack(_elementStack), 139 | didStartParsingElement = elementObservation.didStartParsingElement { 140 | didStartParsingElement(elementName: elementName, attributeDict: attributeDict) 141 | } 142 | } 143 | 144 | public func parser(parser: NSXMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) { 145 | if let elementObservation = elementObservationForElementStack(_elementStack) { 146 | let foundInnerText = elementObservation.foundInnerText 147 | let innerText = elementObservation.innerText 148 | if foundInnerText != nil && innerText != nil { 149 | foundInnerText!(elementName: elementName, text: elementObservation.innerText!) 150 | } 151 | 152 | if let didEndParsingElement = elementObservation.didEndParsingElement { 153 | didEndParsingElement(elementName: elementName) 154 | } 155 | } 156 | 157 | if elementName == _elementStack.last { 158 | _elementStack.removeLast() 159 | } else { 160 | LogError("XML badly formatted!") 161 | parser.abortParsing() 162 | } 163 | } 164 | 165 | public func parser(parser: NSXMLParser, foundCharacters string: String) { 166 | // The parser object may send the delegate several parser:foundCharacters: messages to report the characters of an element. Because string may be only part of the total character content for the current element, you should append it to the current accumulation of characters until the element changes. 167 | 168 | if let elementObservation = elementObservationForElementStack(_elementStack) { 169 | elementObservation.appendInnerText(string) 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Source/UPnP Foundation/GlobalLib.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalLib.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | 26 | func curlRep(request: NSURLRequest) -> String { 27 | var curl = "" 28 | 29 | curl += "curl -i" 30 | 31 | if let httpMethod = request.HTTPMethod { 32 | curl += " -X \(httpMethod)" 33 | } 34 | 35 | if let httpBody = request.HTTPBody, 36 | body = NSString(data: httpBody, encoding: NSUTF8StringEncoding) { 37 | curl += " -d '" 38 | curl += "\(body)" 39 | curl += "'" 40 | } 41 | 42 | if let allHTTPHeaderFields = request.allHTTPHeaderFields { 43 | for (key, value) in allHTTPHeaderFields { 44 | curl += " -H '\(key): \(value)'" 45 | } 46 | } 47 | 48 | curl += " \"\(request.URL)\"" 49 | 50 | return curl 51 | } 52 | 53 | public typealias Error = NSError 54 | 55 | public enum Result { 56 | case Success(T) 57 | case Failure(Error) 58 | 59 | init(_ value: T) { 60 | self = .Success(value) 61 | } 62 | 63 | init(_ error: Error) { 64 | self = .Failure(error) 65 | } 66 | 67 | var failed: Bool { 68 | if case .Failure(_) = self { 69 | return true 70 | } 71 | return false 72 | } 73 | 74 | var error: Error? { 75 | if case .Failure(let error) = self { 76 | return error 77 | } 78 | return nil 79 | } 80 | 81 | var value: T? { 82 | if case .Success(let value) = self { 83 | return value 84 | } 85 | return nil 86 | } 87 | } 88 | 89 | public enum EmptyResult { 90 | case Success 91 | case Failure(Error) 92 | 93 | init() { 94 | self = .Success 95 | } 96 | 97 | init(_ error: Error) { 98 | self = .Failure(error) 99 | } 100 | 101 | var failed: Bool { 102 | if case .Failure(_) = self { 103 | return true 104 | } 105 | return false 106 | } 107 | 108 | var error: Error? { 109 | if case .Failure(let error) = self { 110 | return error 111 | } 112 | return nil 113 | } 114 | } 115 | 116 | func createError(message: String) -> Error { 117 | return NSError(domain: "UPnAtom", code: 0, userInfo: [NSLocalizedDescriptionKey: message]) 118 | } 119 | 120 | extension RangeReplaceableCollectionType where Generator.Element : Equatable { 121 | mutating func removeObject(object: Generator.Element) -> Generator.Element? { 122 | if let found = self.indexOf(object) { 123 | return removeAtIndex(found) 124 | } 125 | return nil 126 | } 127 | } 128 | 129 | extension NSError { 130 | /// An alternative to iOS's [NSError localizedDescription] which returns an esoteric Cocoa error when [NSError userInfo][NSLocalizedDescriptionKey] is nil. In that case, this method will return nil instead. 131 | var localizedDescriptionOrNil: String? { 132 | return self.userInfo[NSLocalizedDescriptionKey] as? String 133 | } 134 | 135 | func localizedDescription(defaultDescription: String) -> String { 136 | return self.localizedDescriptionOrNil != nil ? self.localizedDescriptionOrNil! : defaultDescription 137 | } 138 | } 139 | 140 | /// Until Apple provides modules for all of the missing C libraries, there's no good way to perform this task in Swift. 141 | //func getIFAddresses() -> [String: String] { 142 | // var addresses = [String: String]() 143 | // 144 | // // Get list of all interfaces on the local machine: 145 | // var ifaddr : UnsafeMutablePointer = nil 146 | // if getifaddrs(&ifaddr) == 0 { 147 | // 148 | // // For each interface ... 149 | // for (var ptr = ifaddr; ptr != nil; ptr = ptr.memory.ifa_next) { 150 | // let flags = Int32(ptr.memory.ifa_flags) 151 | // var addr = ptr.memory.ifa_addr.memory 152 | // let interfaceName = String.fromCString(ptr.memory.ifa_name) 153 | // 154 | // // Check for running IPv4, IPv6 interfaces. Skip the loopback interface. 155 | // if (flags & (IFF_UP|IFF_RUNNING|IFF_LOOPBACK)) == (IFF_UP|IFF_RUNNING) { 156 | // if addr.sa_family == UInt8(AF_INET) || addr.sa_family == UInt8(AF_INET6) { 157 | // 158 | // // Convert interface address to a human readable string: 159 | // var hostname = [CChar](count: Int(NI_MAXHOST), repeatedValue: 0) 160 | // if (getnameinfo(&addr, socklen_t(addr.sa_len), &hostname, socklen_t(hostname.count), 161 | // nil, socklen_t(0), NI_NUMERICHOST) == 0) { 162 | // let address = String.fromCString(hostname) 163 | // if address != nil && interfaceName != nil { 164 | // addresses[interfaceName!] = address! 165 | // } 166 | // } 167 | // } 168 | // } 169 | // } 170 | // freeifaddrs(ifaddr) 171 | // } 172 | // 173 | // return addresses 174 | //} 175 | 176 | extension NSTimer { 177 | private class NSTimerClosureHandler { 178 | var closure: () -> Void 179 | 180 | init(closure: () -> Void) { 181 | self.closure = closure 182 | } 183 | 184 | dynamic func fire() { 185 | closure() 186 | } 187 | } 188 | 189 | convenience init(timeInterval: NSTimeInterval, repeats: Bool, closure: (() -> Void)) { 190 | let closureHandler = NSTimerClosureHandler(closure: closure) 191 | self.init(timeInterval: timeInterval, target: closureHandler, selector: "fire", userInfo: nil, repeats: repeats) 192 | } 193 | 194 | class func scheduledTimerWithTimeInterval(timeInterval: NSTimeInterval, repeats: Bool, closure: (() -> Void)) -> NSTimer { 195 | let closureHandler = NSTimerClosureHandler(closure: closure) 196 | return self.scheduledTimerWithTimeInterval(timeInterval, target: closureHandler, selector: "fire", userInfo: nil, repeats: repeats) 197 | } 198 | } 199 | 200 | extension NSArray { 201 | func firstUsingPredicate(predicate: NSPredicate) -> T? { 202 | return self.filteredArrayUsingPredicate(predicate).first as? T 203 | } 204 | } 205 | 206 | extension NSUUID { 207 | var dashlessUUIDString: String { 208 | return self.UUIDString.stringByReplacingOccurrencesOfString("-", withString: "", options: .LiteralSearch) 209 | } 210 | } 211 | 212 | struct Stack { 213 | var elements = [T]() 214 | 215 | mutating func push(element: T) { 216 | elements.append(element) 217 | } 218 | 219 | mutating func pop() -> T { 220 | return elements.removeLast() 221 | } 222 | 223 | func peek() -> T? { 224 | return elements.last 225 | } 226 | 227 | func isEmpty() -> Bool { 228 | return elements.isEmpty 229 | } 230 | } 231 | 232 | func + (left: Dictionary, right: Dictionary) -> Dictionary { 233 | var result = Dictionary() 234 | 235 | for (k, v) in left { 236 | result[k] = v 237 | } 238 | 239 | for (k, v) in right { 240 | result[k] = v 241 | } 242 | 243 | return result 244 | } 245 | 246 | func += (inout left: Dictionary, right: Dictionary) { 247 | for (k, v) in right { 248 | left[k] = v 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /Source/AV Profile/Services/ContentDirectory1Objects.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentDirectory1Object.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | import Ono 26 | 27 | // MARK: ContentDirectory1Object 28 | 29 | /// TODO: For now rooting to NSObject to expose to Objective-C, see Github issue #16 30 | public class ContentDirectory1Object: NSObject { 31 | public let objectID: String 32 | public let parentID: String 33 | public let title: String 34 | public let rawType: String 35 | public let albumArtURL: NSURL? 36 | 37 | init?(xmlElement: ONOXMLElement) { 38 | if let objectID = xmlElement.valueForAttribute("id") as? String, 39 | parentID = xmlElement.valueForAttribute("parentID") as? String, 40 | title = xmlElement.firstChildWithTag("title").stringValue(), 41 | rawType = xmlElement.firstChildWithTag("class").stringValue() { 42 | self.objectID = objectID 43 | self.parentID = parentID 44 | self.title = title 45 | self.rawType = rawType 46 | 47 | if let albumArtURLString = xmlElement.firstChildWithTag("albumArtURI")?.stringValue() { 48 | self.albumArtURL = NSURL(string: albumArtURLString) 49 | } else { albumArtURL = nil } 50 | } else { 51 | /// TODO: Remove default initializations to simply return nil, see Github issue #11 52 | objectID = "" 53 | parentID = "" 54 | title = "" 55 | rawType = "" 56 | albumArtURL = nil 57 | super.init() 58 | return nil 59 | } 60 | 61 | super.init() 62 | } 63 | } 64 | 65 | extension ContentDirectory1Object: ExtendedPrintable { 66 | #if os(iOS) 67 | public var className: String { return "\(self.dynamicType)" } 68 | #elseif os(OSX) // NSObject.className actually exists on OSX! Who knew. 69 | override public var className: String { return "\(self.dynamicType)" } 70 | #endif 71 | override public var description: String { 72 | var properties = PropertyPrinter() 73 | properties.add("id", property: objectID) 74 | properties.add("parentID", property: parentID) 75 | properties.add("title", property: title) 76 | properties.add("class", property: rawType) 77 | properties.add("albumArtURI", property: albumArtURL?.absoluteString) 78 | return properties.description 79 | } 80 | } 81 | 82 | // MARK: - ContentDirectory1Container 83 | 84 | public class ContentDirectory1Container: ContentDirectory1Object { 85 | public let childCount: Int? 86 | 87 | override init?(xmlElement: ONOXMLElement) { 88 | self.childCount = Int(String(xmlElement.valueForAttribute("childCount"))) 89 | 90 | super.init(xmlElement: xmlElement) 91 | } 92 | } 93 | 94 | /// for objective-c type checking 95 | extension ContentDirectory1Object { 96 | public func isContentDirectory1Container() -> Bool { 97 | return self is ContentDirectory1Container 98 | } 99 | } 100 | 101 | /// overrides ExtendedPrintable protocol implementation 102 | extension ContentDirectory1Container { 103 | override public var className: String { return "\(self.dynamicType)" } 104 | override public var description: String { 105 | var properties = PropertyPrinter() 106 | properties.add(super.className, property: super.description) 107 | properties.add("childCount", property: "\(childCount)") 108 | return properties.description 109 | } 110 | } 111 | 112 | // MARK: - ContentDirectory1Item 113 | 114 | public class ContentDirectory1Item: ContentDirectory1Object { 115 | public let resourceURL: NSURL! 116 | 117 | override init?(xmlElement: ONOXMLElement) { 118 | /// TODO: Return nil immediately instead of waiting, see Github issue #11 119 | if let resourceURLString = xmlElement.firstChildWithTag("res").stringValue() { 120 | resourceURL = NSURL(string: resourceURLString) 121 | } else { resourceURL = nil } 122 | 123 | super.init(xmlElement: xmlElement) 124 | 125 | guard resourceURL != nil else { 126 | return nil 127 | } 128 | } 129 | } 130 | 131 | /// for objective-c type checking 132 | extension ContentDirectory1Object { 133 | public func isContentDirectory1Item() -> Bool { 134 | return self is ContentDirectory1Item 135 | } 136 | } 137 | 138 | /// overrides ExtendedPrintable protocol implementation 139 | extension ContentDirectory1Item { 140 | override public var className: String { return "\(self.dynamicType)" } 141 | override public var description: String { 142 | var properties = PropertyPrinter() 143 | properties.add(super.className, property: super.description) 144 | properties.add("resourceURL", property: resourceURL?.absoluteString) 145 | return properties.description 146 | } 147 | } 148 | 149 | // MARK: - ContentDirectory1VideoItem 150 | 151 | public class ContentDirectory1VideoItem: ContentDirectory1Item { 152 | public let bitrate: Int? 153 | public let duration: NSTimeInterval? 154 | public let audioChannelCount: Int? 155 | public let protocolInfo: String? 156 | public let resolution: CGSize? 157 | public let sampleFrequency: Int? 158 | public let size: Int? 159 | 160 | override init?(xmlElement: ONOXMLElement) { 161 | bitrate = Int(String(xmlElement.firstChildWithTag("res").valueForAttribute("bitrate"))) 162 | 163 | if let durationString = xmlElement.firstChildWithTag("res").valueForAttribute("duration") as? String { 164 | let durationComponents = durationString.componentsSeparatedByString(":") 165 | var count: Double = 0 166 | var duration: Double = 0 167 | for durationComponent in durationComponents.reverse() { 168 | duration += (durationComponent as NSString).doubleValue * pow(60, count) 169 | count++ 170 | } 171 | 172 | self.duration = NSTimeInterval(duration) 173 | } else { self.duration = nil } 174 | 175 | audioChannelCount = Int(String(xmlElement.firstChildWithTag("res").valueForAttribute("nrAudioChannels"))) 176 | 177 | protocolInfo = xmlElement.firstChildWithTag("res").valueForAttribute("protocolInfo") as? String 178 | 179 | if let resolutionComponents = (xmlElement.firstChildWithTag("res").valueForAttribute("resolution") as? String)?.componentsSeparatedByString("x"), 180 | width = Int(String(resolutionComponents.first)), 181 | height = Int(String(resolutionComponents.last)) { 182 | resolution = CGSize(width: width, height: height) 183 | } else { resolution = nil } 184 | 185 | sampleFrequency = Int(String(xmlElement.firstChildWithTag("res").valueForAttribute("sampleFrequency"))) 186 | 187 | size = Int(String(xmlElement.firstChildWithTag("res").valueForAttribute("size"))) 188 | 189 | super.init(xmlElement: xmlElement) 190 | } 191 | } 192 | 193 | /// for objective-c type checking 194 | extension ContentDirectory1Object { 195 | public func isContentDirectory1VideoItem() -> Bool { 196 | return self is ContentDirectory1VideoItem 197 | } 198 | } 199 | 200 | /// overrides ExtendedPrintable protocol implementation 201 | extension ContentDirectory1VideoItem { 202 | override public var className: String { return "\(self.dynamicType)" } 203 | override public var description: String { 204 | var properties = PropertyPrinter() 205 | properties.add(super.className, property: super.description) 206 | properties.add("bitrate", property: "\(bitrate)") 207 | properties.add("duration", property: "\(duration)") 208 | properties.add("audioChannelCount", property: "\(audioChannelCount)") 209 | properties.add("protocolInfo", property: protocolInfo) 210 | properties.add("resolution", property: "\(resolution?.width)x\(resolution?.height)") 211 | properties.add("sampleFrequency", property: "\(sampleFrequency)") 212 | properties.add("size", property: "\(size)") 213 | return properties.description 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /Examples/ControlPointDemo_ObjC/ControlPointDemo/Player.m: -------------------------------------------------------------------------------- 1 | // 2 | // Player.m 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | #import "Player.h" 25 | @import UPnAtom; 26 | 27 | typedef NS_ENUM(NSInteger, PlayerState) { 28 | PlayerStateUnknown = 0, 29 | PlayerStateStopped, 30 | PlayerStatePlaying, 31 | PlayerStatePaused 32 | }; 33 | 34 | @interface Player () 35 | @property (nonatomic, readwrite) NSArray *playlist; 36 | @property (nonatomic, readwrite) UIBarButtonItem *playPauseButton; 37 | @property (nonatomic, readwrite) UIBarButtonItem *stopButton; 38 | @property (nonatomic) PlayerState playerState; 39 | @end 40 | 41 | @implementation Player { 42 | NSInteger _position; 43 | id _avTransportEventObserver; 44 | NSString *_avTransportInstanceID; 45 | } 46 | 47 | + (Player *)sharedInstance { 48 | static Player *instance = nil; 49 | static dispatch_once_t onceToken; 50 | 51 | dispatch_once(&onceToken, ^{ 52 | instance = [Player new]; 53 | 54 | instance.playPauseButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"play_button"] style:UIBarButtonItemStylePlain target:instance action:@selector(playPauseButtonTapped:)]; 55 | instance.stopButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"stop_button"] style:UIBarButtonItemStylePlain target:instance action:@selector(stopButtonTapped:)]; 56 | instance->_avTransportInstanceID = @"0"; 57 | }); 58 | 59 | return instance; 60 | } 61 | 62 | - (void)setMediaRenderer:(MediaRenderer1Device *)mediaRenderer { 63 | MediaRenderer1Device *oldRenderer = _mediaRenderer; 64 | _mediaRenderer = mediaRenderer; 65 | 66 | if (_avTransportEventObserver != nil) { 67 | [oldRenderer.avTransportService removeEventObserver:_avTransportEventObserver]; 68 | } 69 | 70 | _avTransportEventObserver = [mediaRenderer.avTransportService addEventObserver:[NSOperationQueue currentQueue] callBackBlock:^(UPnPEvent *event) { 71 | if ([event.service isAVTransport1Service] && [event isAVTransport1Event]) { 72 | AVTransport1Event *avTransportEvent = (AVTransport1Event *)event; 73 | NSLog(@"%@ Event: %@", event.service.className, avTransportEvent.instanceState); 74 | NSString *transportState = [avTransportEvent.instanceState[@"TransportState"] lowercaseString]; 75 | if (transportState.length) { 76 | if ([transportState rangeOfString:@"playing"].location != NSNotFound) { 77 | self.playerState = PlayerStatePlaying; 78 | } 79 | else if ([transportState rangeOfString:@"paused"].location != NSNotFound) { 80 | self.playerState = PlayerStatePaused; 81 | } 82 | else if ([transportState rangeOfString:@"stopped"].location != NSNotFound) { 83 | self.playerState = PlayerStateStopped; 84 | } 85 | else { 86 | self.playerState = PlayerStateUnknown; 87 | } 88 | } 89 | } 90 | }]; 91 | } 92 | 93 | - (void)startPlayback:(NSArray *)playList position:(NSInteger)position{ 94 | [self setPlaylist:playList]; 95 | 96 | [self startPlayback:position]; 97 | } 98 | 99 | - (void)startPlayback:(NSInteger)position{ 100 | //Do we have a Renderer & a playlist ? 101 | if(_mediaRenderer == nil || _playlist == nil){ 102 | return; 103 | } 104 | 105 | if(position >= [_playlist count]){ 106 | position = 0; //Loop 107 | } 108 | 109 | _position = position; 110 | 111 | ContentDirectory1Object *item = _playlist[position]; 112 | //Is it a Media1ServerItem ? 113 | if([item isContentDirectory1VideoItem]){ 114 | ContentDirectory1VideoItem *item = _playlist[position]; 115 | 116 | NSString *uri = [item resourceURL].absoluteString; 117 | if (uri.length) { 118 | __weak typeof(self) weakSelf = self; 119 | [self.mediaRenderer.avTransportService setAVTransportURIWithInstanceID:_avTransportInstanceID currentURI:uri currentURIMetadata:@"" success:^{ 120 | NSLog(@"URI Set succeeded!"); 121 | 122 | [weakSelf playWithSuccess:^{ 123 | NSLog(@"Play command succeeded!"); 124 | } failure:^(NSError *error) { 125 | NSLog(@"Play command failed: %@", error.localizedDescription); 126 | }]; 127 | } failure:^(NSError *error) { 128 | NSLog(@"URI Set failed: %@", error.localizedDescription); 129 | }]; 130 | } 131 | } 132 | } 133 | 134 | #pragma mark - Internal lib 135 | 136 | - (void)playPauseButtonTapped:(id)sender { 137 | switch (self.playerState) { 138 | case PlayerStatePlaying: 139 | [self pauseWithSuccess:^{ 140 | NSLog(@"Pause command succeeded!"); 141 | } failure:^(NSError *error) { 142 | NSLog(@"Pause command failed: %@", error.localizedDescription); 143 | }]; 144 | break; 145 | 146 | case PlayerStatePaused: 147 | [self playWithSuccess:^{ 148 | NSLog(@"Play command succeeded!"); 149 | } failure:^(NSError *error) { 150 | NSLog(@"Play command failed: %@", error.localizedDescription); 151 | }]; 152 | break; 153 | 154 | case PlayerStateStopped: 155 | [self playWithSuccess:^{ 156 | NSLog(@"Play command succeeded!"); 157 | } failure:^(NSError *error) { 158 | NSLog(@"Play command failed: %@", error.localizedDescription); 159 | }]; 160 | break; 161 | 162 | default: 163 | NSLog(@"Play/Pause button cannot be used in this state."); 164 | break; 165 | } 166 | } 167 | 168 | - (void)stopButtonTapped:(id)sender { 169 | switch (self.playerState) { 170 | case PlayerStatePlaying: 171 | [self stopWithSuccess:^{ 172 | NSLog(@"Stop command succeeded!"); 173 | } failure:^(NSError *error) { 174 | NSLog(@"Stop command failed: %@", error.localizedDescription); 175 | }]; 176 | break; 177 | 178 | case PlayerStatePaused: 179 | [self stopWithSuccess:^{ 180 | NSLog(@"Stop command succeeded!"); 181 | } failure:^(NSError *error) { 182 | NSLog(@"Stop command failed: %@", error.localizedDescription); 183 | }]; 184 | break; 185 | 186 | case PlayerStateStopped: 187 | NSLog(@"Stop button cannot be used in this state."); 188 | break; 189 | 190 | default: 191 | NSLog(@"Stop button cannot be used in this state."); 192 | break; 193 | } 194 | } 195 | 196 | - (void)setPlayerState:(PlayerState)playerState { 197 | _playerState = playerState; 198 | 199 | switch (playerState) { 200 | case PlayerStateStopped: 201 | self.playPauseButton.image = [UIImage imageNamed:@"play_button"]; 202 | break; 203 | 204 | case PlayerStatePlaying: 205 | self.playPauseButton.image = [UIImage imageNamed:@"pause_button"]; 206 | break; 207 | 208 | case PlayerStatePaused: 209 | self.playPauseButton.image = [UIImage imageNamed:@"play_button"]; 210 | break; 211 | 212 | default: 213 | self.playPauseButton.image = [UIImage imageNamed:@"play_button"]; 214 | break; 215 | } 216 | } 217 | 218 | - (void)playWithSuccess:(void (^)(void))success failure:(void (^)(NSError *))failure { 219 | [self.mediaRenderer.avTransportService playWithInstanceID:_avTransportInstanceID speed:@"1" success:success failure:failure]; 220 | } 221 | 222 | - (void)pauseWithSuccess:(void (^)(void))success failure:(void (^)(NSError *))failure { 223 | [self.mediaRenderer.avTransportService pauseWithInstanceID:_avTransportInstanceID success:success failure:failure]; 224 | } 225 | 226 | - (void)stopWithSuccess:(void (^)(void))success failure:(void (^)(NSError *))failure { 227 | [self.mediaRenderer.avTransportService stopWithInstanceID:_avTransportInstanceID success:success failure:failure]; 228 | } 229 | 230 | @end 231 | -------------------------------------------------------------------------------- /Source/SSDP/Explorer/SSDPExplorer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SSDPExplorer.swift 3 | // 4 | // Copyright (c) 2015 David Robles 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | import CocoaAsyncSocket 26 | import AFNetworking 27 | 28 | protocol SSDPExplorerDelegate: class { 29 | func ssdpExplorer(explorer: SSDPExplorer, didMakeDiscovery discovery: SSDPDiscovery) 30 | // Removed discoveries will have an invalid desciption URL 31 | func ssdpExplorer(explorer: SSDPExplorer, didRemoveDiscovery discovery: SSDPDiscovery) 32 | /// Assume explorer has stopped after a failure. 33 | func ssdpExplorer(explorer: SSDPExplorer, didFailWithError error: NSError) 34 | } 35 | 36 | class SSDPExplorer { 37 | enum SSDPMessageType { 38 | case SearchResponse 39 | case AvailableNotification 40 | case UpdateNotification 41 | case UnavailableNotification 42 | } 43 | 44 | weak var delegate: SSDPExplorerDelegate? 45 | 46 | // private 47 | private static let _multicastGroupAddress = "239.255.255.250" 48 | private static let _multicastUDPPort: UInt16 = 1900 49 | private var _multicastSocket: GCDAsyncUdpSocket? // TODO: Should ideally be a constant, see Github issue #10 50 | private var _unicastSocket: GCDAsyncUdpSocket? // TODO: Should ideally be a constant, see Github issue #10 51 | private var _types = [SSDPType]() // TODO: Should ideally be a Set, see Github issue #13 52 | 53 | func startExploring(forTypes types: [SSDPType], onInterface interface: String = "en0") -> EmptyResult { 54 | assert(_multicastSocket == nil, "Socket is already open, stop it first!") 55 | 56 | // create sockets 57 | guard let multicastSocket: GCDAsyncUdpSocket! = GCDAsyncUdpSocket(delegate: self, delegateQueue: dispatch_get_main_queue()), 58 | unicastSocket: GCDAsyncUdpSocket! = GCDAsyncUdpSocket(delegate: self, delegateQueue: dispatch_get_main_queue()) else { 59 | return .Failure(createError("Socket could not be created")) 60 | } 61 | _multicastSocket = multicastSocket 62 | _unicastSocket = unicastSocket 63 | multicastSocket.setIPv6Enabled(false) 64 | unicastSocket.setIPv6Enabled(false) 65 | 66 | // Configure unicast socket 67 | // Bind to address on the specified interface to a random port to receive unicast datagrams 68 | do { 69 | try unicastSocket.bindToPort(0, interface: interface) 70 | } catch { 71 | stopExploring() 72 | return .Failure(createError("Could not bind socket to port")) 73 | } 74 | 75 | do { 76 | try unicastSocket.beginReceiving() 77 | } catch { 78 | stopExploring() 79 | return .Failure(createError("Could not begin receiving error")) 80 | } 81 | 82 | // Configure multicast socket 83 | // Bind to port without defining the interface to bind to the address INADDR_ANY (0.0.0.0). This prevents any address filtering which allows datagrams sent to the multicast group to be receives 84 | do { 85 | try multicastSocket.bindToPort(SSDPExplorer._multicastUDPPort) 86 | } catch { 87 | stopExploring() 88 | return .Failure(createError("Could not bind socket to multicast port")) 89 | } 90 | 91 | // Join multicast group to express interest to router of receiving multicast datagrams 92 | do { 93 | try multicastSocket.joinMulticastGroup(SSDPExplorer._multicastGroupAddress) 94 | } catch { 95 | stopExploring() 96 | return .Failure(createError("Could not join multicast group")) 97 | } 98 | 99 | do { 100 | try multicastSocket.beginReceiving() 101 | } catch { 102 | stopExploring() 103 | return .Failure(createError("Could not begin receiving error")) 104 | } 105 | 106 | _types = types 107 | for type in types { 108 | if let data = searchRequestData(forType: type) { 109 | // println(">>>> SENDING SEARCH REQUEST\n\(NSString(data: data, encoding: NSUTF8StringEncoding))") 110 | unicastSocket.sendData(data, toHost: SSDPExplorer._multicastGroupAddress, port: SSDPExplorer._multicastUDPPort, withTimeout: -1, tag: type.hashValue) 111 | } 112 | } 113 | 114 | return .Success 115 | } 116 | 117 | func stopExploring() { 118 | _multicastSocket?.close() 119 | _multicastSocket = nil 120 | _unicastSocket?.close() 121 | _unicastSocket = nil 122 | _types = [] 123 | } 124 | 125 | private func searchRequestData(forType type: SSDPType) -> NSData? { 126 | var requestBody = [ 127 | "M-SEARCH * HTTP/1.1", 128 | "HOST: \(SSDPExplorer._multicastGroupAddress):\(SSDPExplorer._multicastUDPPort)", 129 | "MAN: \"ssdp:discover\"", 130 | "ST: \(type.rawValue)", 131 | "MX: 3"] 132 | 133 | if let userAgent = AFHTTPRequestSerializer().valueForHTTPHeaderField("User-Agent") { 134 | requestBody += ["USER-AGENT: \(userAgent)\r\n\r\n\r\n"] 135 | } 136 | 137 | let requestBodyString = requestBody.joinWithSeparator("\r\n") 138 | return requestBodyString.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false) 139 | } 140 | 141 | private func notifyDelegate(ofFailure error: NSError) { 142 | dispatch_async(dispatch_get_main_queue(), { [unowned self] () -> Void in 143 | self.delegate?.ssdpExplorer(self, didFailWithError: error) 144 | }) 145 | } 146 | 147 | private func notifyDelegate(discovery: SSDPDiscovery, added: Bool) { 148 | dispatch_async(dispatch_get_main_queue(), { [unowned self] () -> Void in 149 | added ? self.delegate?.ssdpExplorer(self, didMakeDiscovery: discovery) : self.delegate?.ssdpExplorer(self, didRemoveDiscovery: discovery) 150 | }) 151 | } 152 | 153 | private func handleSSDPMessage(messageType: SSDPMessageType, headers: [String: String]) { 154 | if let usnRawValue = headers["usn"], 155 | usn = UniqueServiceName(rawValue: usnRawValue), 156 | locationString = headers["location"], 157 | locationURL = NSURL(string: locationString), 158 | /// NT = Notification Type - SSDP discovered from device advertisements 159 | /// ST = Search Target - SSDP discovered as a result of using M-SEARCH requests 160 | ssdpTypeRawValue = (headers["st"] != nil ? headers["st"] : headers["nt"]), 161 | ssdpType = SSDPType(rawValue: ssdpTypeRawValue) where _types.indexOf(ssdpType) != nil { 162 | LogVerbose("SSDP response headers: \(headers)") 163 | let discovery = SSDPDiscovery(usn: usn, descriptionURL: locationURL, type: ssdpType) 164 | switch messageType { 165 | case .SearchResponse, .AvailableNotification, .UpdateNotification: 166 | notifyDelegate(discovery, added: true) 167 | case .UnavailableNotification: 168 | notifyDelegate(discovery, added: false) 169 | } 170 | } 171 | } 172 | } 173 | 174 | extension SSDPExplorer: GCDAsyncUdpSocketDelegate { 175 | @objc func udpSocket(sock: GCDAsyncUdpSocket!, didNotSendDataWithTag tag: Int, dueToError error: NSError!) { 176 | stopExploring() 177 | 178 | // this case should always have an error 179 | notifyDelegate(ofFailure: error ?? createError("Did not send SSDP message.")) 180 | } 181 | 182 | @objc func udpSocketDidClose(sock: GCDAsyncUdpSocket!, withError error: NSError!) { 183 | if let error = error { 184 | notifyDelegate(ofFailure: error) 185 | } 186 | } 187 | 188 | @objc func udpSocket(sock: GCDAsyncUdpSocket!, didReceiveData data: NSData!, fromAddress address: NSData!, withFilterContext filterContext: AnyObject!) { 189 | if let message = NSString(data: data, encoding: NSUTF8StringEncoding) as? String { 190 | // println({ () -> String in 191 | // let socketType = (sock === self._unicastSocket) ? "UNICAST" : "MULTICAST" 192 | // return "<<<< RECEIVED ON \(socketType) SOCKET\n\(message)" 193 | // }()) 194 | var httpMethodLine: String? 195 | var headers = [String: String]() 196 | let headersRegularExpression = try? NSRegularExpression(pattern: "^([a-z0-9-]+): *(.+)$", options: [.CaseInsensitive, .AnchorsMatchLines]) 197 | message.enumerateLines({ (line, stop) -> () in 198 | if httpMethodLine == nil { 199 | httpMethodLine = line 200 | } else { 201 | headersRegularExpression?.enumerateMatchesInString(line, options: [], range: NSRange(location: 0, length: line.characters.count), usingBlock: { (resultOptional: NSTextCheckingResult?, flags: NSMatchingFlags, stop: UnsafeMutablePointer) -> Void in 202 | if let result = resultOptional where result.numberOfRanges == 3 { 203 | let key = (line as NSString).substringWithRange(result.rangeAtIndex(1)).lowercaseString 204 | let value = (line as NSString).substringWithRange(result.rangeAtIndex(2)) 205 | headers[key] = value 206 | } 207 | }) 208 | } 209 | }) 210 | 211 | if let httpMethodLine = httpMethodLine { 212 | let nts = headers["nts"] 213 | switch (httpMethodLine, nts) { 214 | case ("HTTP/1.1 200 OK", _): 215 | handleSSDPMessage(.SearchResponse, headers: headers) 216 | case ("NOTIFY * HTTP/1.1", .Some(let notificationType)) where notificationType == "ssdp:alive": 217 | handleSSDPMessage(.AvailableNotification, headers: headers) 218 | case ("NOTIFY * HTTP/1.1", .Some(let notificationType)) where notificationType == "ssdp:update": 219 | handleSSDPMessage(.UpdateNotification, headers: headers) 220 | case ("NOTIFY * HTTP/1.1", .Some(let notificationType)) where notificationType == "ssdp:byebye": 221 | headers["location"] = headers["host"] // byebye messages don't have a location 222 | handleSSDPMessage(.UnavailableNotification, headers: headers) 223 | default: 224 | return 225 | } 226 | } 227 | } 228 | } 229 | } 230 | --------------------------------------------------------------------------------