VLCOpenNetworkStreamViewController.m 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. /*****************************************************************************
  2. * VLCOpenNetworkStreamViewController.m
  3. * VLC for iOS
  4. *****************************************************************************
  5. * Copyright (c) 2013-2018 VideoLAN. All rights reserved.
  6. * $Id$
  7. *
  8. * Authors: Felix Paul Kühne <fkuehne # videolan.org>
  9. * Gleb Pinigin <gpinigin # gmail.com>
  10. * Pierre Sagaspe <pierre.sagaspe # me.com>
  11. * Adam Viaud <mcnight # mcnight.fr>
  12. * Refer to the COPYING file of the official project for license.
  13. *****************************************************************************/
  14. #import "VLCOpenNetworkStreamViewController.h"
  15. #import "VLCPlaybackController.h"
  16. #import "VLCLibraryViewController.h"
  17. #import "VLCStreamingHistoryCell.h"
  18. #import "UIDevice+VLC.h"
  19. #import "VLC_iOS-Swift.h"
  20. @interface VLCOpenNetworkStreamViewController () <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate, VLCStreamingHistoryCellMenuItemProtocol>
  21. {
  22. NSMutableArray *_recentURLs;
  23. NSMutableDictionary *_recentURLTitles;
  24. }
  25. @end
  26. @implementation VLCOpenNetworkStreamViewController
  27. + (void)initialize
  28. {
  29. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  30. NSDictionary *appDefaults = @{kVLCRecentURLs : @[], kVLCRecentURLTitles : @{}, kVLCPrivateWebStreaming : @(NO)};
  31. [defaults registerDefaults:appDefaults];
  32. }
  33. - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
  34. {
  35. self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
  36. if (self) {
  37. self.title = NSLocalizedString(@"OPEN_NETWORK", comment: "");
  38. }
  39. return self;
  40. }
  41. - (void)applicationDidBecomeActive:(NSNotification *)notification
  42. {
  43. [self updatePasteboardTextInURLField];
  44. }
  45. - (void)ubiquitousKeyValueStoreDidChange:(NSNotification *)notification
  46. {
  47. /* TODO: don't blindly trust that the Cloud knows best */
  48. _recentURLs = [NSMutableArray arrayWithArray:[[NSUbiquitousKeyValueStore defaultStore] arrayForKey:kVLCRecentURLs]];
  49. _recentURLTitles = [NSMutableDictionary dictionaryWithDictionary:[[NSUbiquitousKeyValueStore defaultStore] dictionaryForKey:kVLCRecentURLTitles]];
  50. [self.historyTableView reloadData];
  51. }
  52. - (void)viewDidLoad
  53. {
  54. [super viewDidLoad];
  55. NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
  56. [notificationCenter addObserver:self
  57. selector:@selector(ubiquitousKeyValueStoreDidChange:)
  58. name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
  59. object:[NSUbiquitousKeyValueStore defaultStore]];
  60. [notificationCenter addObserver:self
  61. selector:@selector(updateForTheme)
  62. name:kVLCThemeDidChangeNotification
  63. object:nil];
  64. /* force store update */
  65. NSUbiquitousKeyValueStore *ubiquitousKeyValueStore = [NSUbiquitousKeyValueStore defaultStore];
  66. [ubiquitousKeyValueStore synchronize];
  67. /* fetch data from cloud */
  68. _recentURLs = [NSMutableArray arrayWithArray:[[NSUbiquitousKeyValueStore defaultStore] arrayForKey:kVLCRecentURLs]];
  69. _recentURLTitles = [NSMutableDictionary dictionaryWithDictionary:[[NSUbiquitousKeyValueStore defaultStore] dictionaryForKey:kVLCRecentURLTitles]];
  70. /* merge data from local storage (aka legacy VLC versions) */
  71. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  72. NSArray *localRecentUrls = [defaults objectForKey:kVLCRecentURLs];
  73. if (localRecentUrls != nil) {
  74. if (localRecentUrls.count != 0) {
  75. [_recentURLs addObjectsFromArray:localRecentUrls];
  76. [defaults setObject:nil forKey:kVLCRecentURLs];
  77. [ubiquitousKeyValueStore setArray:_recentURLs forKey:kVLCRecentURLs];
  78. [ubiquitousKeyValueStore synchronize];
  79. }
  80. }
  81. /*
  82. * Observe changes to the pasteboard so we can automatically paste it into the URL field.
  83. * Do not use UIPasteboardChangedNotification because we have copy actions that will trigger it on this screen.
  84. * Instead when the user comes back to the application from the background (or the inactive state by pulling down notification center), update the URL field.
  85. * Using the 'active' rather than 'foreground' notification for future proofing if iOS ever allows running multiple apps on the same screen (which would allow the pasteboard to be changed without truly backgrounding the app).
  86. */
  87. [notificationCenter addObserver:self
  88. selector:@selector(applicationDidBecomeActive:)
  89. name:UIApplicationDidBecomeActiveNotification
  90. object:[UIApplication sharedApplication]];
  91. [self.openButton setTitle:NSLocalizedString(@"OPEN_NETWORK", nil) forState:UIControlStateNormal];
  92. [self.openButton setAccessibilityIdentifier:@"Open Network Stream"];
  93. [self.privateModeLabel setText:NSLocalizedString(@"PRIVATE_PLAYBACK_TOGGLE", nil)];
  94. UILabel *scanSubModelabel = self.ScanSubModeLabel;
  95. [scanSubModelabel setText:NSLocalizedString(@"SCAN_SUBTITLE_TOGGLE", nil)];
  96. [scanSubModelabel setAdjustsFontSizeToFitWidth:YES];
  97. [scanSubModelabel setNumberOfLines:0];
  98. [self.whatToOpenHelpLabel setText:NSLocalizedString(@"OPEN_NETWORK_HELP", nil)];
  99. self.urlField.delegate = self;
  100. self.urlField.keyboardType = UIKeyboardTypeURL;
  101. self.edgesForExtendedLayout = UIRectEdgeNone;
  102. // This will be called every time this VC is opened by the side menu controller
  103. [self updatePasteboardTextInURLField];
  104. // Registering a custom menu item for renaming streams
  105. NSString *renameTitle = NSLocalizedString(@"BUTTON_RENAME", nil);
  106. SEL renameStreamSelector = @selector(renameStream:);
  107. UIMenuItem *renameItem = [[UIMenuItem alloc] initWithTitle:renameTitle action:renameStreamSelector];
  108. UIMenuController *sharedMenuController = [UIMenuController sharedMenuController];
  109. [sharedMenuController setMenuItems:@[renameItem]];
  110. [sharedMenuController update];
  111. [self updateForTheme];
  112. }
  113. - (NSString *)detailText
  114. {
  115. return NSLocalizedString(@"STREAMVC_DETAILTEXT", nil);
  116. }
  117. - (UIImage *)cellImage
  118. {
  119. return [UIImage imageNamed:@"OpenNetStream"];
  120. }
  121. - (void)updateForTheme
  122. {
  123. self.historyTableView.backgroundColor = PresentationTheme.current.colors.background;
  124. self.view.backgroundColor = PresentationTheme.current.colors.background;
  125. NSAttributedString *coloredAttributedPlaceholder = [[NSAttributedString alloc] initWithString:@"http://myserver.com/file.mkv" attributes:@{NSForegroundColorAttributeName: PresentationTheme.current.colors.lightTextColor}];
  126. self.urlField.attributedPlaceholder = coloredAttributedPlaceholder;
  127. self.urlField.backgroundColor = PresentationTheme.current.colors.cellBackgroundB;
  128. self.urlField.textColor = PresentationTheme.current.colors.cellTextColor;
  129. self.privateModeLabel.textColor = PresentationTheme.current.colors.lightTextColor;
  130. self.ScanSubModeLabel.textColor = PresentationTheme.current.colors.lightTextColor;
  131. self.whatToOpenHelpLabel.textColor = PresentationTheme.current.colors.lightTextColor;
  132. self.openButton.backgroundColor = PresentationTheme.current.colors.orangeUI;
  133. [self.historyTableView reloadData];
  134. }
  135. - (void)updatePasteboardTextInURLField
  136. {
  137. UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
  138. if ([pasteboard containsPasteboardTypes:@[@"public.url"]])
  139. self.urlField.text = [[pasteboard valueForPasteboardType:@"public.url"] absoluteString];
  140. }
  141. - (void)viewWillAppear:(BOOL)animated
  142. {
  143. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  144. self.privateToggleSwitch.on = [defaults boolForKey:kVLCPrivateWebStreaming];
  145. self.ScanSubToggleSwitch.on = [defaults boolForKey:kVLChttpScanSubtitle];
  146. [super viewWillAppear:animated];
  147. }
  148. - (void)viewWillDisappear:(BOOL)animated
  149. {
  150. [[NSNotificationCenter defaultCenter] removeObserver:self
  151. name:UIApplicationDidBecomeActiveNotification
  152. object:[UIApplication sharedApplication]];
  153. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  154. [defaults setBool:self.privateToggleSwitch.on forKey:kVLCPrivateWebStreaming];
  155. [defaults setBool:self.ScanSubToggleSwitch.on forKey:kVLChttpScanSubtitle];
  156. [self.view endEditing:YES];
  157. /* force update before we leave */
  158. [[NSUbiquitousKeyValueStore defaultStore] synchronize];
  159. [super viewWillDisappear:animated];
  160. }
  161. - (CGSize)contentSizeForViewInPopover {
  162. return [self.view sizeThatFits:CGSizeMake(320, 800)];
  163. }
  164. #pragma mark - UI interaction
  165. - (BOOL)shouldAutorotate
  166. {
  167. UIInterfaceOrientation toInterfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];
  168. if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone && toInterfaceOrientation == UIInterfaceOrientationPortraitUpsideDown)
  169. return NO;
  170. return YES;
  171. }
  172. - (IBAction)openButtonAction:(id)sender
  173. {
  174. if ([self.urlField.text length] <= 0 || [NSURL URLWithString:self.urlField.text] == nil) {
  175. UIAlertController *alertController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"URL_NOT_SUPPORTED", nil)
  176. message:nil
  177. preferredStyle:UIAlertControllerStyleAlert];
  178. UIAlertAction *okAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"BUTTON_OK", nil)
  179. style:UIAlertActionStyleCancel
  180. handler:nil];
  181. [alertController addAction:okAction];
  182. [self presentViewController:alertController animated:YES completion:nil];
  183. return;
  184. }
  185. if (!self.privateToggleSwitch.on) {
  186. NSString *urlString = self.urlField.text;
  187. if ([_recentURLs indexOfObject:urlString] != NSNotFound)
  188. [_recentURLs removeObject:urlString];
  189. if (_recentURLs.count >= 100)
  190. [_recentURLs removeLastObject];
  191. [_recentURLs addObject:urlString];
  192. [[NSUbiquitousKeyValueStore defaultStore] setArray:_recentURLs forKey:kVLCRecentURLs];
  193. [self.historyTableView reloadData];
  194. }
  195. [self.urlField resignFirstResponder];
  196. [self _openURLStringAndDismiss:self.urlField.text];
  197. }
  198. - (void)renameStreamFromCell:(UITableViewCell *)cell {
  199. NSIndexPath *cellIndexPath = [self.historyTableView indexPathForCell:cell];
  200. NSString *renameString = NSLocalizedString(@"BUTTON_RENAME", nil);
  201. NSString *cancelString = NSLocalizedString(@"BUTTON_CANCEL", nil);
  202. UIAlertController *alertController = [UIAlertController alertControllerWithTitle:renameString
  203. message:nil
  204. preferredStyle:UIAlertControllerStyleAlert];
  205. UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:cancelString
  206. style:UIAlertActionStyleCancel
  207. handler:nil];
  208. UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
  209. NSString *streamTitle = alertController.textFields.firstObject.text;
  210. [self renameStreamWithTitle:streamTitle atIndex:cellIndexPath.row];
  211. }];
  212. [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
  213. textField.text = cell.textLabel.text;
  214. [[NSNotificationCenter defaultCenter] addObserverForName:UITextFieldTextDidChangeNotification
  215. object:textField
  216. queue:[NSOperationQueue mainQueue]
  217. usingBlock:^(NSNotification * _Nonnull note) {
  218. okAction.enabled = (textField.text.length != 0);
  219. }];
  220. }];
  221. [alertController addAction:cancelAction];
  222. [alertController addAction:okAction];
  223. [self presentViewController:alertController animated:YES completion:nil];
  224. }
  225. - (void)renameStreamWithTitle:(NSString *)title atIndex:(NSInteger)index
  226. {
  227. [_recentURLTitles setObject:title forKey:[@(index) stringValue]];
  228. [[NSUbiquitousKeyValueStore defaultStore] setDictionary:_recentURLTitles forKey:kVLCRecentURLTitles];
  229. [[NSOperationQueue mainQueue] addOperationWithBlock:^{
  230. [self.historyTableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationAutomatic];
  231. }];
  232. }
  233. #pragma mark - table view data source
  234. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
  235. {
  236. return 1;
  237. }
  238. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  239. {
  240. return _recentURLs.count;
  241. }
  242. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  243. {
  244. static NSString *CellIdentifier = @"StreamingHistoryCell";
  245. VLCStreamingHistoryCell *cell = (VLCStreamingHistoryCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
  246. if (cell == nil) {
  247. cell = [[VLCStreamingHistoryCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
  248. cell.delegate = self;
  249. [cell customizeAppearance];
  250. }
  251. NSString *content = [_recentURLs[indexPath.row] stringByRemovingPercentEncoding];
  252. NSString *possibleTitle = _recentURLTitles[[@(indexPath.row) stringValue]];
  253. cell.detailTextLabel.text = content;
  254. cell.textLabel.text = possibleTitle ?: [content lastPathComponent];
  255. return cell;
  256. }
  257. #pragma mark - table view delegate
  258. - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
  259. {
  260. cell.backgroundColor = (indexPath.row % 2 == 0)? PresentationTheme.current.colors.cellBackgroundB : PresentationTheme.current.colors.cellBackgroundA;
  261. cell.textLabel.textColor = PresentationTheme.current.colors.cellTextColor;
  262. cell.detailTextLabel.textColor = PresentationTheme.current.colors.cellDetailTextColor;
  263. }
  264. - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
  265. {
  266. return YES;
  267. }
  268. - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
  269. {
  270. if (editingStyle == UITableViewCellEditingStyleDelete) {
  271. [_recentURLs removeObjectAtIndex:indexPath.row];
  272. [_recentURLTitles removeObjectForKey:[@(indexPath.row) stringValue]];
  273. [[NSUbiquitousKeyValueStore defaultStore] setArray:_recentURLs forKey:kVLCRecentURLs];
  274. [[NSUbiquitousKeyValueStore defaultStore] setDictionary:_recentURLTitles forKey:kVLCRecentURLTitles];
  275. [tableView reloadData];
  276. }
  277. }
  278. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
  279. {
  280. [self.historyTableView deselectRowAtIndexPath:indexPath animated:NO];
  281. [self _openURLStringAndDismiss:_recentURLs[indexPath.row]];
  282. }
  283. - (void)tableView:(UITableView *)tableView
  284. performAction:(SEL)action
  285. forRowAtIndexPath:(NSIndexPath *)indexPath
  286. withSender:(id)sender
  287. {
  288. NSString *actionText = NSStringFromSelector(action);
  289. if ([actionText isEqualToString:@"copy:"])
  290. [UIPasteboard generalPasteboard].string = _recentURLs[indexPath.row];
  291. }
  292. - (BOOL)tableView:(UITableView *)tableView
  293. canPerformAction:(SEL)action
  294. forRowAtIndexPath:(NSIndexPath *)indexPath
  295. withSender:(id)sender
  296. {
  297. NSString *actionText = NSStringFromSelector(action);
  298. if ([actionText isEqualToString:@"copy:"])
  299. return YES;
  300. return NO;
  301. }
  302. - (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
  303. {
  304. return YES;
  305. }
  306. #pragma mark - internals
  307. - (void)_openURLStringAndDismiss:(NSString *)url
  308. {
  309. NSURL *URLscheme = [NSURL URLWithString:url];
  310. NSString *URLofSubtitle = nil;
  311. if ([URLscheme.scheme isEqualToString:@"http"] && self.ScanSubToggleSwitch.on) {
  312. URLofSubtitle = [self _checkURLofSubtitle:url];
  313. }
  314. VLCMedia *media = [VLCMedia mediaWithURL:[NSURL URLWithString:url]];
  315. VLCMediaList *medialist = [[VLCMediaList alloc] init];
  316. [medialist addMedia:media];
  317. [[VLCPlaybackController sharedInstance] playMediaList:medialist firstIndex:0 subtitlesFilePath:URLofSubtitle];
  318. }
  319. - (NSString *)_checkURLofSubtitle:(NSString *)url
  320. {
  321. NSCharacterSet *characterFilter = [NSCharacterSet characterSetWithCharactersInString:@"\\.():$"];
  322. NSString *subtitleFileExtensions = [[kSupportedSubtitleFileExtensions componentsSeparatedByCharactersInSet:characterFilter] componentsJoinedByString:@""];
  323. NSArray *arraySubtitleFileExtensions = [subtitleFileExtensions componentsSeparatedByString:@"|"];
  324. NSString *urlTemp = [[url stringByDeletingPathExtension] stringByAppendingString:@"."];
  325. NSUInteger count = arraySubtitleFileExtensions.count;
  326. for (int i = 0; i < count; i++) {
  327. NSString *checkAddress = [urlTemp stringByAppendingString:arraySubtitleFileExtensions[i]];
  328. NSURL *checkURL = [NSURL URLWithString:checkAddress];
  329. NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:checkURL];
  330. request.HTTPMethod = @"HEAD";
  331. NSURLResponse *response = nil;
  332. NSError *error = nil;
  333. [self sendSynchronousRequest:request returningResponse:&response error:&error];
  334. NSInteger httpStatus = [(NSHTTPURLResponse *)response statusCode];
  335. if (httpStatus == 200) {
  336. NSString *fileSubtitlePath = [self _getFileSubtitleFromServer:checkURL];
  337. return fileSubtitlePath;
  338. }
  339. }
  340. return nil;
  341. }
  342. - (NSString *)_getFileSubtitleFromServer:(NSURL *)url
  343. {
  344. NSString *fileSubtitlePath = nil;
  345. NSString *fileName = [[url path] lastPathComponent];
  346. NSData *receivedSub = [NSData dataWithContentsOfURL:url];
  347. if (receivedSub.length < [[UIDevice currentDevice] VLCFreeDiskSpace].longLongValue) {
  348. NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
  349. NSString *directoryPath = [searchPaths objectAtIndex:0];
  350. fileSubtitlePath = [directoryPath stringByAppendingPathComponent:fileName];
  351. NSFileManager *fileManager = [NSFileManager defaultManager];
  352. if (![fileManager fileExistsAtPath:fileSubtitlePath]) {
  353. [fileManager createFileAtPath:fileSubtitlePath contents:nil attributes:nil];
  354. if (![fileManager fileExistsAtPath:fileSubtitlePath])
  355. APLog(@"file creation failed, no data was saved");
  356. }
  357. [receivedSub writeToFile:fileSubtitlePath atomically:YES];
  358. } else {
  359. VLCAlertView *alert = [[VLCAlertView alloc] initWithTitle:NSLocalizedString(@"DISK_FULL", nil)
  360. message:[NSString stringWithFormat:NSLocalizedString(@"DISK_FULL_FORMAT", nil), fileName, [[UIDevice currentDevice] model]]
  361. delegate:self
  362. cancelButtonTitle:NSLocalizedString(@"BUTTON_OK", nil)
  363. otherButtonTitles:nil];
  364. [alert performSelectorOnMainThread:@selector(show) withObject:nil waitUntilDone:NO];
  365. }
  366. return fileSubtitlePath;
  367. }
  368. - (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error
  369. {
  370. NSError __block *erreur = NULL;
  371. NSData __block *data;
  372. BOOL __block reqProcessed = false;
  373. NSURLResponse __block *urlResponse;
  374. [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable _data, NSURLResponse * _Nullable _response, NSError * _Nullable _error) {
  375. urlResponse = _response;
  376. erreur = _error;
  377. data = _data;
  378. reqProcessed = true;
  379. }] resume];
  380. while (!reqProcessed) {
  381. [NSThread sleepForTimeInterval:0];
  382. }
  383. *response = urlResponse;
  384. *error = erreur;
  385. return data;
  386. }
  387. #pragma mark - text view delegate
  388. - (BOOL)textFieldShouldReturn:(UITextField *)textField
  389. {
  390. [self.urlField resignFirstResponder];
  391. return NO;
  392. }
  393. @end