Commit fc761876 authored by Tobias's avatar Tobias

replace VLCUPnPServerListViewController with VLCNetworkServicebrowserUPnP

replace VLCUPnPServerListViewController with VLCNetworkServicebrowserUPnP
parent c90b9e4b
......@@ -39,13 +39,14 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly, nullable) id<VLCNetworkServerBrowser> containerBrowser;
@property (nonatomic, readonly) NSString *name;
@property (nonatomic, readonly) NSURL *URL;
@property (nonatomic, readonly, nullable) NSURL *URL;
@property (nonatomic, readonly, nullable) NSNumber *fileSizeBytes;
@optional
@property (nonatomic, readonly, nullable) NSString *duration;
@property (nonatomic, readonly, nullable) NSURL *subtitleURL;
@property (nonatomic, readonly, nullable) NSURL *thumbnailURL;
@property (nonatomic, getter=isDownloadable, readonly) BOOL downloadable;
@end
......
/*****************************************************************************
* VLCNetworkServerBrowserUPnP.h
* VLC for iOS
*****************************************************************************
* Copyright (c) 2015 VideoLAN. All rights reserved.
* $Id$
*
* Authors: Tobias Conradi <videolan # tobias-conradi.de>
*
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
#import "VLCNetworkServerBrowser-Protocol.h"
NS_ASSUME_NONNULL_BEGIN
@class MediaServer1Device;
@interface VLCNetworkServerBrowserUPnP : NSObject <VLCNetworkServerBrowser>
- (id)initWithUPNPDevice:(MediaServer1Device *)device header:(NSString *)header andRootID:(NSString *)rootID NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
@end
@class MediaServer1BasicObject;
@interface VLCNetworkServerBrowserItemUPnP : NSObject <VLCNetworkServerBrowserItem>
- (instancetype)initWithBasicObject:(MediaServer1BasicObject *)basicObject device:(MediaServer1Device *)device;
@property (nonatomic, readonly, nullable) NSString *duration;
@property (nonatomic, readonly, nullable) NSURL *subtitleURL;
@property (nonatomic, readonly, nullable) NSURL *thumbnailURL;
@property (nonatomic, getter=isDownloadable, readonly) BOOL downloadable;
// UPnP specificis
@property (nonatomic, readonly, nullable) UIImage *image;
@end
#pragma mark - Multi Ressource
@class MediaServer1ItemObject;
@interface VLCNetworkServerBrowserUPnPMultiRessource : NSObject <VLCNetworkServerBrowser>
- (instancetype)initWithItem:(MediaServer1ItemObject *)itemObject device:(MediaServer1Device *)device;
@end
@interface VLCNetworkServerBrowserItemUPnPMultiRessource : NSObject <VLCNetworkServerBrowserItem>
- (instancetype)initWithTitle:(NSString *)title url:(NSURL *)url;
@end
NS_ASSUME_NONNULL_END
\ No newline at end of file
/*****************************************************************************
* VLCNetworkServerBrowserUPnP.m
* VLC for iOS
*****************************************************************************
* Copyright (c) 2015 VideoLAN. All rights reserved.
* $Id$
*
* Authors: Tobias Conradi <videolan # tobias-conradi.de>
*
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
#import "VLCNetworkServerBrowserUPnP.h"
#import "MediaServerBasicObjectParser.h"
#import "MediaServer1ItemObject.h"
#import "MediaServer1ContainerObject.h"
#import "MediaServer1Device.h"
#import "BasicUPnPDevice+VLC.h"
@interface VLCNetworkServerBrowserUPnP ()
@property (nonatomic, readonly) MediaServer1Device *upnpDevice;
@property (nonatomic, readonly) NSString *upnpRootID;
@property (nonatomic, readonly) NSOperationQueue *upnpQueue;
@property (nonatomic, readwrite) NSArray<id<VLCNetworkServerBrowserItem>> *items;
@end
@implementation VLCNetworkServerBrowserUPnP
@synthesize title = _title, delegate = _delegate, items = _items;
- (instancetype)initWithUPNPDevice:(MediaServer1Device *)device header:(NSString *)header andRootID:(NSString *)upnpRootID
{
self = [super init];
if (self) {
_upnpDevice = device;
_title = header;
_upnpRootID = upnpRootID;
_upnpQueue = [[NSOperationQueue alloc] init];
_upnpQueue.maxConcurrentOperationCount = 1;
_upnpQueue.name = @"org.videolan.vlc-ios.upnp.update";
_items = [NSArray array];
}
return self;
}
- (void)update {
[self.upnpQueue addOperationWithBlock:^{
NSString *sortCriteria = @"";
NSMutableString *outSortCaps = [[NSMutableString alloc] init];
[[self.upnpDevice contentDirectory] GetSortCapabilitiesWithOutSortCaps:outSortCaps];
if ([outSortCaps rangeOfString:@"dc:title"].location != NSNotFound)
{
sortCriteria = @"+dc:title";
}
NSMutableString *outResult = [[NSMutableString alloc] init];
NSMutableString *outNumberReturned = [[NSMutableString alloc] init];
NSMutableString *outTotalMatches = [[NSMutableString alloc] init];
NSMutableString *outUpdateID = [[NSMutableString alloc] init];
[[self.upnpDevice contentDirectory] BrowseWithObjectID:self.upnpRootID BrowseFlag:@"BrowseDirectChildren" Filter:@"*" StartingIndex:@"0" RequestedCount:@"0" SortCriteria:sortCriteria OutResult:outResult OutNumberReturned:outNumberReturned OutTotalMatches:outTotalMatches OutUpdateID:outUpdateID];
NSData *didl = [outResult dataUsingEncoding:NSUTF8StringEncoding];
MediaServerBasicObjectParser *parser;
NSMutableArray *objectsArray = [[NSMutableArray alloc] init];
parser = [[MediaServerBasicObjectParser alloc] initWithMediaObjectArray:objectsArray itemsOnly:NO];
[parser parseFromData:didl];
NSMutableArray *itemsArray = [[NSMutableArray alloc] init];
for (MediaServer1BasicObject *object in objectsArray) {
[itemsArray addObject:[[VLCNetworkServerBrowserItemUPnP alloc] initWithBasicObject:object device:self.upnpDevice]];
}
self.items = [itemsArray copy];
[self.delegate networkServerBrowserDidUpdate:self];
}];
}
@end
@interface MediaServer1ItemObject (VLC)
@end
@implementation MediaServer1ItemObject (VLC)
- (id)vlc_ressourceItemForKey:(NSString *)key urlString:(NSString *)urlString device:(MediaServer1Device *)device {
// Provide users with a descriptive action sheet for them to choose based on the multiple resources advertised by DLNA devices (HDHomeRun for example)
NSRange position = [key rangeOfString:@"http-get:*:video/"];
if (position.location == NSNotFound)
return nil;
NSString *orgPNValue;
NSString *transcodeValue;
// Attempt to parse DLNA.ORG_PN first
NSArray *components = [key componentsSeparatedByString:@";"];
NSArray *nonFlagsComponents = [components[0] componentsSeparatedByString:@":"];
NSString *orgPN = [nonFlagsComponents lastObject];
// Check to see if we are where we should be
NSRange orgPNRange = [orgPN rangeOfString:@"DLNA.ORG_PN="];
if (orgPNRange.location == 0) {
orgPNValue = [orgPN substringFromIndex:orgPNRange.length];
}
// HDHomeRun: Get the transcode profile from the HTTP API if possible
if ([device VLC_isHDHomeRunMediaServer]) {
NSRange transcodeRange = [urlString rangeOfString:@"transcode="];
if (transcodeRange.location != NSNotFound) {
transcodeValue = [urlString substringFromIndex:transcodeRange.location + transcodeRange.length];
// Check that there are no more parameters
NSRange ampersandRange = [transcodeValue rangeOfString:@"&"];
if (ampersandRange.location != NSNotFound) {
transcodeValue = [transcodeValue substringToIndex:transcodeRange.location];
}
transcodeValue = [transcodeValue capitalizedString];
}
}
// Fallbacks to get the most descriptive resource title
NSString *profileTitle;
if ([transcodeValue length] && [orgPNValue length]) {
profileTitle = [NSString stringWithFormat:@"%@ (%@)", transcodeValue, orgPNValue];
// The extra whitespace is to get UIActionSheet to render the text better (this bug has been fixed in iOS 8)
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
if (!SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
profileTitle = [NSString stringWithFormat:@" %@ ", profileTitle];
}
}
} else if ([transcodeValue length]) {
profileTitle = transcodeValue;
} else if ([orgPNValue length]) {
profileTitle = orgPNValue;
} else if ([key length]) {
profileTitle = key;
} else if ([urlString length]) {
profileTitle = urlString;
} else {
profileTitle = NSLocalizedString(@"UNKNOWN", nil);
}
return [[VLCNetworkServerBrowserItemUPnPMultiRessource alloc] initWithTitle:profileTitle url:[NSURL URLWithString:urlString]];
}
- (NSArray *)vlc_ressourceItemsForDevice:(MediaServer1Device *)device {
// Store it so we can act on the action sheet callback.
NSMutableArray *array = [NSMutableArray array];
[uriCollection enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, NSString * _Nonnull urlString, BOOL * _Nonnull stop) {
id item = [self vlc_ressourceItemForKey:key urlString:urlString device:device];
if (item) {
[array addObject:item];
}
}];
return [array copy];
}
@end
@interface VLCNetworkServerBrowserItemUPnP ()
@property (nonatomic, readonly) MediaServer1BasicObject *mediaServerObject;
@property (nonatomic, readonly) MediaServer1Device *upnpDevice;
@end
@implementation VLCNetworkServerBrowserItemUPnP
@synthesize container = _container, name = _name, URL = _URL, fileSizeBytes = _fileSizeBytes;
- (instancetype)initWithBasicObject:(MediaServer1BasicObject *)basicObject device:(nonnull MediaServer1Device *)device
{
self = [super init];
if (self) {
_mediaServerObject = basicObject;
_upnpDevice = device;
_name = basicObject.title;
_thumbnailURL = [NSURL URLWithString:basicObject.albumArt];
_fileSizeBytes = nil;
_duration = nil;
_URL = nil;
_container = basicObject.isContainer;
if (!_container && [basicObject isKindOfClass:[MediaServer1ItemObject class]]) {
MediaServer1ItemObject *mediaItem = (MediaServer1ItemObject *)basicObject;
long long mediaSize = 0;
unsigned int durationInSeconds = 0;
unsigned int bitrate = 0;
for (MediaServer1ItemRes *resource in mediaItem.resources) {
if (resource.bitrate > 0 && resource.durationInSeconds > 0) {
mediaSize = resource.size;
durationInSeconds = resource.durationInSeconds;
bitrate = resource.bitrate;
}
}
if (mediaSize < 1)
mediaSize = [mediaItem.size integerValue];
if (mediaSize < 1)
mediaSize = (bitrate * durationInSeconds);
// object.item.videoItem.videoBroadcast items (like the HDHomeRun) may not have this information. Center the title (this makes channel names look better for the HDHomeRun)
if (mediaSize > 0) {
_fileSizeBytes = @(mediaSize);
}
if (durationInSeconds > 0) {
_duration = [VLCTime timeWithInt:durationInSeconds * 1000].stringValue;
}
NSArray<NSString *>* protocolStrings = [[mediaItem uriCollection] allKeys];
protocolStrings = [protocolStrings filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString * _Nonnull evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
return [evaluatedObject containsString:@"http-get:*:video/"];
}]];
if (protocolStrings.count == 1) {
_URL = [NSURL URLWithString:[mediaItem uri]];
} else if (protocolStrings.count > 1) {
// whith mutlple playable ressources we simulate to be a container
_container = YES;
}
}
}
return self;
}
- (BOOL)isContainer {
return self.mediaServerObject.isContainer;
}
- (BOOL)isDownloadable {
// Disable downloading for the HDHomeRun for now to avoid infinite downloads (URI needs a duration parameter, otherwise you are just downloading a live stream). VLC also needs an extension in the file name for this to work.
BOOL downloadable = ![self.upnpDevice VLC_isHDHomeRunMediaServer];
return downloadable;
}
- (id<VLCNetworkServerBrowser>)containerBrowser {
MediaServer1BasicObject *basicObject = self.mediaServerObject;
if (basicObject.isContainer) {
return [[VLCNetworkServerBrowserUPnP alloc] initWithUPNPDevice:self.upnpDevice header:self.mediaServerObject.title andRootID:self.mediaServerObject.objectID];
} else if ([basicObject isKindOfClass:[MediaServer1ItemObject class]]) {
return [[VLCNetworkServerBrowserUPnPMultiRessource alloc] initWithItem:(MediaServer1ItemObject *)self.mediaServerObject device:self.upnpDevice];
} else {
return nil;
}
}
- (UIImage *)image {
UIImage *broadcastImage = nil;
// Custom TV icon for video broadcasts
if ([[self.mediaServerObject objectClass] isEqualToString:@"object.item.videoItem.videoBroadcast"]) {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
broadcastImage = [UIImage imageNamed:@"TVBroadcastIcon"];
} else {
broadcastImage = [UIImage imageNamed:@"TVBroadcastIcon~ipad"];
}
}
return broadcastImage;
}
@end
#pragma mark - Multi Ressource
@implementation VLCNetworkServerBrowserUPnPMultiRessource
@synthesize items = _items, title = _title, delegate = _delegate;
- (instancetype)initWithItem:(MediaServer1ItemObject *)itemObject device:(MediaServer1Device *)device
{
self = [super init];
if (self) {
_title = [itemObject title];
_items = [itemObject vlc_ressourceItemsForDevice:device];
}
return self;
}
- (void) update {
[self.delegate networkServerBrowserDidUpdate:self];
}
@end
@implementation VLCNetworkServerBrowserItemUPnPMultiRessource
@synthesize URL = _URL, container = _container, fileSizeBytes = _fileSizeBytes, name =_name;
- (instancetype)initWithTitle:(NSString *)title url:(NSURL *)url
{
self = [super init];
if (self) {
_name = title;
_URL = url;
_container = NO;
_fileSizeBytes = nil;
}
return self;
}
- (id<VLCNetworkServerBrowser>)containerBrowser {
return nil;
}
@end
/*****************************************************************************
* VLCUPnPServerListViewController.h
* VLC for iOS
*****************************************************************************
* Copyright (c) 2015 VideoLAN. All rights reserved.
* $Id$
*
* Author: Felix Paul Kühne <fkuehne # videolan.org>
*
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
#import "VLCNetworkListViewController.h"
@class MediaServer1Device;
@interface VLCUPnPServerListViewController : VLCNetworkListViewController
- (id)initWithUPNPDevice:(MediaServer1Device *)device header:(NSString *)header andRootID:(NSString *)rootID;
@end
/*****************************************************************************
* VLCUPnPServerListViewController.m
* VLC for iOS
*****************************************************************************
* Copyright (c) 2013-2015 VideoLAN. All rights reserved.
* $Id$
*
* Authors: Felix Paul Kühne <fkuehne # videolan.org>
* Marc Etcheverry <marc@taplightsoftware.com>
* Pierre SAGASPE <pierre.sagaspe # me.com>
*
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
#import "VLCUPnPServerListViewController.h"
#import "VLCDownloadViewController.h"
#import "VLCNetworkListCell.h"
#import "VLCPlaybackController.h"
#import "VLCStatusLabel.h"
#import "NSString+SupportedMedia.h"
#import "MediaServerBasicObjectParser.h"
#import "MediaServer1ItemObject.h"
#import "MediaServer1ContainerObject.h"
#import "MediaServer1Device.h"
#import "BasicUPnPDevice+VLC.h"
@interface VLCUPnPServerListViewController () <VLCNetworkListCellDelegate, UITableViewDataSource, UITableViewDelegate, UIActionSheetDelegate>
{
MediaServer1Device *_UPNPdevice;
NSString *_UPNProotID;
NSMutableArray *_mutableObjectList;
NSMutableArray *_searchData;
MediaServer1ItemObject *_lastSelectedMediaItem;
UIView *_resourceSelectionActionSheetAnchorView;
}
@end
@implementation VLCUPnPServerListViewController
- (id)initWithUPNPDevice:(MediaServer1Device*)device header:(NSString*)header andRootID:(NSString*)rootID
{
self = [super init];
if (self) {
_UPNPdevice = device;
self.title = header;
_UPNProotID = rootID;
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
@synchronized(self) {
_mutableObjectList = [[NSMutableArray alloc] init];
}
NSString *sortCriteria = @"";
NSMutableString *outSortCaps = [[NSMutableString alloc] init];
[[_UPNPdevice contentDirectory] GetSortCapabilitiesWithOutSortCaps:outSortCaps];
if ([outSortCaps rangeOfString:@"dc:title"].location != NSNotFound)
{
sortCriteria = @"+dc:title";
}
NSMutableString *outResult = [[NSMutableString alloc] init];
NSMutableString *outNumberReturned = [[NSMutableString alloc] init];
NSMutableString *outTotalMatches = [[NSMutableString alloc] init];
NSMutableString *outUpdateID = [[NSMutableString alloc] init];
[[_UPNPdevice contentDirectory] BrowseWithObjectID:_UPNProotID BrowseFlag:@"BrowseDirectChildren" Filter:@"*" StartingIndex:@"0" RequestedCount:@"0" SortCriteria:sortCriteria OutResult:outResult OutNumberReturned:outNumberReturned OutTotalMatches:outTotalMatches OutUpdateID:outUpdateID];
@synchronized(self) {
[_mutableObjectList removeAllObjects];
}
NSData *didl = [outResult dataUsingEncoding:NSUTF8StringEncoding];
MediaServerBasicObjectParser *parser;
@synchronized(self) {
parser = [[MediaServerBasicObjectParser alloc] initWithMediaObjectArray:_mutableObjectList itemsOnly:NO];
}
[parser parseFromData:didl];
}
#pragma mark - table view data source, for more see super
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSUInteger count;
if (tableView == self.searchDisplayController.searchResultsTableView) {
@synchronized(self) {
count = _searchData.count;
}
} else {
@synchronized(self) {
count = _mutableObjectList.count;
}
}
return count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
VLCNetworkListCell *cell = (VLCNetworkListCell *)[tableView dequeueReusableCellWithIdentifier:VLCNetworkListCellIdentifier];
if (cell == nil)
cell = [VLCNetworkListCell cellWithReuseIdentifier:VLCNetworkListCellIdentifier];
MediaServer1BasicObject *item;
if (tableView == self.searchDisplayController.searchResultsTableView) {
@synchronized(self) {
item = _searchData[indexPath.row];
}
} else {
@synchronized(self) {
item = _mutableObjectList[indexPath.row];
}
}
if (![item isContainer]) {
MediaServer1ItemObject *mediaItem;
long long mediaSize = 0;
unsigned int durationInSeconds = 0;
unsigned int bitrate = 0;
if (tableView == self.searchDisplayController.searchResultsTableView) {
@synchronized(self) {
mediaItem = _searchData[indexPath.row];
}
} else {
@synchronized(self) {
mediaItem = _mutableObjectList[indexPath.row];
}
}
MediaServer1ItemRes *resource = nil;
NSEnumerator *e = [[mediaItem resources] objectEnumerator];
while((resource = (MediaServer1ItemRes*)[e nextObject])){
if (resource.bitrate > 0 && resource.durationInSeconds > 0) {
mediaSize = resource.size;
durationInSeconds = resource.durationInSeconds;
bitrate = resource.bitrate;
}
}
if (mediaSize < 1)
mediaSize = [mediaItem.size longLongValue];
if (mediaSize < 1)
mediaSize = (bitrate * durationInSeconds);
// object.item.videoItem.videoBroadcast items (like the HDHomeRun) may not have this information. Center the title (this makes channel names look better for the HDHomeRun)
if (mediaSize > 0 && durationInSeconds > 0) {
[cell setSubtitle: [NSString stringWithFormat:@"%@ (%@)", [NSByteCountFormatter stringFromByteCount:mediaSize countStyle:NSByteCountFormatterCountStyleFile], [VLCTime timeWithInt:durationInSeconds * 1000].stringValue]];
} else {
cell.titleLabelCentered = YES;
}
// Custom TV icon for video broadcasts
if ([[mediaItem objectClass] isEqualToString:@"object.item.videoItem.videoBroadcast"]) {
UIImage *broadcastImage;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
broadcastImage = [UIImage imageNamed:@"TVBroadcastIcon"];
} else {
broadcastImage = [UIImage imageNamed:@"TVBroadcastIcon~ipad"];
}
[cell setIcon:broadcastImage];
} else {
[cell setIcon:[UIImage imageNamed:@"blank"]];
}
[cell setIsDirectory:NO];
if (mediaItem.albumArt != nil)
[cell setIconURL:[NSURL URLWithString:mediaItem.albumArt]];
// Disable downloading for the HDHomeRun for now to avoid infinite downloads (URI needs a duration parameter, otherwise you are just downloading a live stream). VLC also needs an extension in the file name for this to work.
if (![_UPNPdevice VLC_isHDHomeRunMediaServer]) {
cell.isDownloadable = YES;
}
cell.delegate = self;
} else {
[cell setIsDirectory:YES];
if (item.albumArt != nil)
[cell setIconURL:[NSURL URLWithString:item.albumArt]];
[cell setIcon:[UIImage imageNamed:@"folder"]];
}
[cell setTitle:[item title]];
return cell;
}
#pragma mark - table view delegate, for more see super
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
MediaServer1BasicObject *item;
if (tableView == self.searchDisplayController.searchResultsTableView) {
@synchronized(self) {
item = _searchData[indexPath.row];
}
} else {
@synchronized(self) {
item = _mutableObjectList[indexPath.row];
}
}
if ([item isContainer]) {
MediaServer1ContainerObject *container;
if (tableView == self.searchDisplayController.searchResultsTableView) {
@synchronized(self) {
container = _searchData[indexPath.row];
}
} else {
@synchronized(self) {
container = _mutableObjectList[indexPath.row];
}
}
VLCUPnPServerListViewController *targetViewController = [[VLCUPnPServerListViewController alloc] initWithUPNPDevice:_UPNPdevice header:[container title] andRootID:[container objectID]];
[[self navigationController] pushViewController:targetViewController animated:YES];
} else {
MediaServer1ItemObject *mediaItem;
if (tableView == self.searchDisplayController.searchResultsTableView) {
@synchronized(self) {
mediaItem = _searchData[indexPath.row];
}
} else {
@synchronized(self) {
mediaItem = _mutableObjectList[indexPath.row];
}
}
NSURL *itemURL;
NSArray *uriCollectionKeys = [[mediaItem uriCollection] allKeys];
NSUInteger count = uriCollectionKeys.count;
NSRange position;
NSUInteger correctIndex = 0;
NSUInteger numberOfDownloadableResources = 0;
for (NSUInteger i = 0; i < count; i++) {
position = [uriCollectionKeys[i] rangeOfString:@"http-get:*:video/"];
if (position.location != NSNotFound) {
correctIndex = i;
numberOfDownloadableResources++;
}
}
NSArray *uriCollectionObjects = [[mediaItem uriCollection] allValues];