VLCNetworkServerBrowserUPnP.m 14.9 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*****************************************************************************
 * 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
31
@synthesize title = _title, delegate = _delegate, items = _items, mediaList = _mediaList;
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

- (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]];
        }

78
79
80
        @synchronized(_items) {
            _items = [itemsArray copy];
        }
81
        _mediaList = [self buildMediaList];
82
83
84
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            [self.delegate networkServerBrowserDidUpdate:self];
        }];
85
86
87
    }];
}

88
- (VLCMediaList *)buildMediaList
89
{
90
    NSMutableArray *mediaArray;
91
    @synchronized(_items) {
92
93
94
        mediaArray = [NSMutableArray new];
        for (id<VLCNetworkServerBrowserItem> item in _items) {
            VLCMedia *media = [item media];
95
            if (media)
96
                [mediaArray addObject:media];
97
98
        }
    }
99
    VLCMediaList *mediaList = [[VLCMediaList alloc] initWithArray:mediaArray];
100
    return mediaList;
101
102
}

103
104
105
106
107
108
109
110
111
112
113
114
@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)

115
    NSRange position = [key rangeOfString:kVLCUPnPVideoProtocolKey];
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242

    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) {
Felix Paul Kühne's avatar
Felix Paul Kühne committed
243
244
245
                if (evaluatedObject == nil || ![evaluatedObject isKindOfClass:[NSString class]])
                    return NO;

246
                if ([evaluatedObject respondsToSelector:@selector(containsString:)]) {
247
                    if ([evaluatedObject containsString:kVLCUPnPVideoProtocolKey])
248
                        return YES;
249
                    if ([evaluatedObject containsString:kVLCUPnPAudioProtocolKey])
250
251
                        return YES;
                } else {
252
                    NSRange foundRange = [evaluatedObject rangeOfString:kVLCUPnPVideoProtocolKey];
253
254
                    if (foundRange.location != NSNotFound)
                        return YES;
255
                    foundRange = [evaluatedObject rangeOfString:kVLCUPnPAudioProtocolKey];
256
257
258
                    if (foundRange.location != NSNotFound)
                        return YES;
                }
259
                return NO;
260
            }]];
261

262
263
264
            // Check for multiple URIs.
            if ([mediaItem.uriCollection count] > 1) {
                for (NSString *key in mediaItem.uriCollection) {
265
266
267
268
269
270
271
272
273
274
275
276
277
278
                    if ([key respondsToSelector:@selector(containsString:)]) {
                        if ([key containsString:kVLCUPnPVideoProtocolKey] || [key containsString:kVLCUPnPAudioProtocolKey]) {
                            mediaItem.uri = [mediaItem.uriCollection objectForKey:key];
                        }
                    } else {
                        NSRange foundRage = [key rangeOfString:kVLCUPnPVideoProtocolKey];
                        if (foundRage.location != NSNotFound) {
                            mediaItem.uri = [mediaItem.uriCollection objectForKey:key];
                        } else {
                            foundRage = [key rangeOfString:kVLCUPnPAudioProtocolKey];
                            if (foundRage.location != NSNotFound) {
                                mediaItem.uri = [mediaItem.uriCollection objectForKey:key];
                            }
                        }
279
280
281
                    }
                }
            }
282
            _URL = [NSURL URLWithString:[mediaItem uri]];
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
        }
    }
    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;
}

321
322
323
- (VLCMedia *)media
{
    if (!_URL)
324
        return [VLCMedia mediaAsNodeWithName:self.name];
325
326
327
328
329
330
331
332

    VLCMedia *media = [VLCMedia mediaWithURL:_URL];
    NSString *title = self.name;
    if (title.length) {
        [media setMetadata:self.name forKey:VLCMetaInformationTitle];
    }

    return media;
333
334
}

335
336
337
338
339
@end

#pragma mark - Multi Ressource

@implementation VLCNetworkServerBrowserUPnPMultiRessource
340
@synthesize items = _items, title = _title, delegate = _delegate, mediaList = _mediaList;
341
342
343
344
345
346
347

- (instancetype)initWithItem:(MediaServer1ItemObject *)itemObject device:(MediaServer1Device *)device
{
    self = [super init];
    if (self) {
        _title = [itemObject title];
        _items = [itemObject vlc_ressourceItemsForDevice:device];
348
        _mediaList = [self buildMediaList];
349
350
351
352
353
354
355
    }
    return self;
}

- (void) update {
    [self.delegate networkServerBrowserDidUpdate:self];
}
356

357
- (VLCMediaList *)buildMediaList
358
{
359
    VLCMediaList *mediaList = [[VLCMediaList alloc] init];
360
    @synchronized(_items) {
361
362
        for (id<VLCNetworkServerBrowserItem> browseritem in _items) {
            VLCMedia *media = [browseritem media];
363
            if (media)
364
                [mediaList addMedia:media];
365
366
        }
    }
367
    return mediaList;
368
369
}

370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
@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;
}

392
393
394
395
- (VLCMedia *)media
{
    if (!_URL)
        return nil;
396
397
398
399
400
401
402
403

    VLCMedia *media = [VLCMedia mediaWithURL:_URL];
    NSString *title = self.name;
    if (title.length) {
        [media setMetadata:self.name forKey:VLCMetaInformationTitle];
    }

    return media;
404
405
}

406
@end