iOS – Can't stream video from Parse Backend

Recently I have created my own parse server hosted on heroku using mongoLab to store my data.

My problem is I am saving a video as a parse PFFile, however I can not seem to be able to stream it after saving it.

  • Play Video From URL in WatchOS 2
  • AVAssetWriterInput, impossible to choose video resolution?
  • iPhone SDK: Custom video player controls
  • UIImagePickerController record video with landscape orientation
  • AVURLAsset refuses to load video
  • iOS AVFoundation: Setting Orientation of Video
  • Here are my exact steps.

    First, I save the video returned by UIImagePicker

    //Get the video URL
    let videoURL = info[UIImagePickerControllerMediaURL] as? NSURL
    
    //Create PFFile with NSData from URL
    let data = NSData(contentsOfURL: videoURL!)
    videoFile = PFFile(data: data!, contentType: "video/mp4")
    
    //Save PFFile first, then save the PFUser
    PFUser.currentUser()?.setObject(videoFile!, forKey: "profileVideo")
                videoFile?.saveInBackgroundWithBlock({ (succeeded, error) -> Void in
                    print("saved video")
                    PFUser.currentUser()?.saveInBackgroundWithBlock({ (succeeded, error) -> Void in
                        if succeeded && error == nil {
                            print("user saved")
    
                            //Hide progress bar 
                            UIView.animateWithDuration(0.5, animations: { () -> Void in                   
                                self.progressBar.alpha = 0
                                }, completion: { (bool) -> Void in
                                    self.progressBar.removeFromSuperview()
                            })
    
                        }else{
    
                            //Show error if the save failed
                            let message = error!.localizedDescription
                            let alert = UIAlertController(title: "Uploading profile picture error!", message: message, preferredStyle: UIAlertControllerStyle.Alert)
                            let dismiss = UIAlertAction(title: "OK", style: UIAlertActionStyle.Cancel, handler: nil)
                            alert.addAction(dismiss)
                            self.presentViewController(alert, animated: true, completion: nil)
    
                        }
                    })
                    }, progressBlock: { (progress) -> Void in
                        self.progressBar.setProgress(Float(progress)/100, animated: true)
                })
    

    This all works well. The problem lies when I retrieve the PFFile and try to stream the video. Here is my code for that:

    //Get URL from my current user
    self.videoFile = PFUser.currentUser()?.objectForKey("profileVideo") as? PFFile
                            self.profileVideoURL = NSURL(string: (self.videoFile?.url)!)
    
    //Create AVPlayerController
    let playerController = AVPlayerViewController()
    
    //Set AVPlayer URL to where the file is stored on the sever
    let avPlayer = AVPlayer(URL: self.profileVideoURL)
    playerController.player = avPlayer
    
    //Present the playerController
    self.presentViewController(playerController, animated: true, completion: { () -> Void in
     playerController.player?.play()
    })
    

    What ends up happening when I present the playerController is this:

    enter image description here

    Why is this happening when I try stream my video?

    Any help is greatly appreciated!

    UPDATE

    I have recently tried playing a video saved from a different database using this line of code: let videoURL = NSURL(string: "https://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")

    This confirms that it is the format I am saving my PFFile in that is causing the error.

    It must be this line causing the error as "video/mp4" is probably not the right format : videoFile = PFFile(data: data!, contentType: "video/mp4")

    UPDATE 2

    I have taken the direct link of my .mp4 file located on mongoLab and found I can play it in google chrome, but not on safari or my iPhone.

    UPDATE 3

    I have found that this was an issue with the parse api itself, and had nothing to do with the code as my code works perfectly when using the original parse backend (the one that is closing down) instead of my custom parse server. I currently have no solution however it should get fixed over time.

    4 Solutions Collect From Internet About “iOS – Can't stream video from Parse Backend”

    this code works for me

    let playerController = AVPlayerViewController()
    
          self.addChildViewController(playerController)
          self.view.addSubview(playerController.view)
          playerController.view.frame = self.view.frame
    
          file!.getDataInBackgroundWithBlock({
            (movieData: NSData?, error: NSError?) -> Void in
            if (error == nil) {
    
              let filemanager = NSFileManager.defaultManager()
    
              let documentsPath : AnyObject = NSSearchPathForDirectoriesInDomains(.DocumentDirectory,.UserDomainMask,true)[0]
              let destinationPath:NSString = documentsPath.stringByAppendingString("/file.mov")
              movieData!.writeToFile ( destinationPath as String, atomically:true)
    
    
              let playerItem = AVPlayerItem(asset: AVAsset(URL: NSURL(fileURLWithPath: destinationPath as String)))
              let player = AVPlayer(playerItem: playerItem)
              playerController.player = player
              player.play()
            } else {
              print ("error on getting movie data \(error?.localizedDescription)")
            }
          })
    

    parse-server doesn’t seem to be supporting streaming in Safari/iOS and the solution is the enable it using express & GridStore as follows,

    parse-server-example\node_modules\parse-server\lib\Routers\FilesRouter

        {
    key: 'getHandler',
    value: function getHandler(req, res, content) {
    var config = new _Config2.default(req.params.appId);
    var filesController = config.filesController;
    var filename = req.params.filename;
    var video = '.mp4'
    var lastFourCharacters = video.substr(video.length - 4);
    if (lastFourCharacters == '.mp4') {
    
      filesController.handleVideoStream(req, res, filename).then(function (data) {
    
        }).catch(function (err) {
          console.log('404FilesRouter');
            res.status(404);
            res.set('Content-Type', 'text/plain');
            res.end('File not found.');
        });
    }else{
    filesController.getFileData(config, filename).then(function (data) {
      res.status(200);
      res.end(data);
    }).catch(function (err) {
        res.status(404);
        res.set('Content-Type', 'text/plain');
        res.end('File not found.');
      });
     }
    }
    } , ...
    

    parse-server-example\node_modules\parse-server\lib\Controllers\FilesController

    _createClass(FilesController, [{
    key: 'getFileData',
    value: function getFileData(config, filename) {
    return this.adapter.getFileData(filename);
    }
    },{
    key: 'handleVideoStream',
    value: function handleVideoStream(req, res, filename) {
    return this.adapter.handleVideoStream(req, res, filename);
    }
    }, ...
    

    parse-server-example\node_modules\parse-server\lib\Adapters\Files\GridStoreAdapter

    ... , {
         key: 'handleVideoStream',
         value: function handleVideoStream(req, res, filename) {
         return this._connect().then(function (database) {
         return _mongodb.GridStore.exist(database, filename).then(function     () {
      var gridStore = new _mongodb.GridStore(database, filename, 'r');
      gridStore.open(function(err, GridFile) {
          if(!GridFile) {
              res.send(404,'Not Found');
              return;
            }
            console.log('filename');
            StreamGridFile(GridFile, req, res);
          });
          });
          })
          }
          }, ...
    

    Bottom of GridStore Adapter

    function StreamGridFile(GridFile, req, res) {
    var buffer_size = 1024 * 1024;//1024Kb
    
    if (req.get('Range') != null) { //was: if(req.headers['range'])
      // Range request, partialle stream the file
      console.log('Range Request');
      var parts = req.get('Range').replace(/bytes=/, "").split("-");
      var partialstart = parts[0];
      var partialend = parts[1];
      var start = partialstart ? parseInt(partialstart, 10) : 0;
      var end = partialend ? parseInt(partialend, 10) : GridFile.length - 1;
      var chunksize = (end - start) + 1;
    
      if(chunksize == 1){
        start = 0;
        partialend = false;
      }
    
      if(!partialend){
        if(((GridFile.length-1) - start) < (buffer_size) ){
            end = GridFile.length - 1;
        }else{
          end = start + (buffer_size);
        }
          chunksize = (end - start) + 1;
        }
    
        if(start == 0 && end == 2){
          chunksize = 1;
        }
    
    res.writeHead(206, {
          'Cache-Control': 'no-cache',
        'Content-Range': 'bytes ' + start + '-' + end + '/' + GridFile.length,
        'Accept-Ranges': 'bytes',
        'Content-Length': chunksize,
        'Content-Type': 'video/mp4',
      });
    
      GridFile.seek(start, function () {
        // get GridFile stream
    
                var stream = GridFile.stream(true);
                var ended = false;
                var bufferIdx = 0;
                var bufferAvail = 0;
                var range = (end - start) + 1;
                var totalbyteswanted = (end - start) + 1;
                var totalbyteswritten = 0;
                // write to response
                stream.on('data', function (buff) {
                bufferAvail += buff.length;
                //Ok check if we have enough to cover our range
                if(bufferAvail < range) {
                //Not enough bytes to satisfy our full range
                    if(bufferAvail > 0)
                    {
                    //Write full buffer
                      res.write(buff);
                      totalbyteswritten += buff.length;
                      range -= buff.length;
                      bufferIdx += buff.length;
                      bufferAvail -= buff.length;
                    }
                }
                else{
    
                //Enough bytes to satisfy our full range!
                    if(bufferAvail > 0) {
                      var buffer = buff.slice(0,range);
                      res.write(buffer);
                      totalbyteswritten += buffer.length;
                      bufferIdx += range;
                      bufferAvail -= range;
                    }
                }
    
                if(totalbyteswritten >= totalbyteswanted) {
                //  totalbytes = 0;
                  GridFile.close();
                  res.end();
                  this.destroy();
                }
                });
            });
    
      }else{
    
    //  res.end(GridFile);
            // stream back whole file
          res.header('Cache-Control', 'no-cache');
          res.header('Connection', 'keep-alive');
          res.header("Accept-Ranges", "bytes");
          res.header('Content-Type', 'video/mp4');
          res.header('Content-Length', GridFile.length);
          var stream = GridFile.stream(true).pipe(res);
       }
      };
    

    P.S
    The original answer is given by @Bragegs here – https://github.com/ParsePlatform/parse-server/issues/1440#issuecomment-212815625 . User @Stav1 has also mentioned it in this thread, but unfortunately he was downvoted ?

    For anyone that lands here still, in looking at the new Parse-Server-example update Parse-server now recognizes streaming; however, you should utilize the Parse iOS sdk method to retrieve the video. The server code is below in case you are rolling out a custom parse-server. I follow with a list of some of the streaming methods.

    Server-code change found in:

    parse-server-example\node_modules\parse-server\lib\Routers\FilesRouter

    {
    key: 'getHandler',
    value: function getHandler(req, res) {
      var config = new _Config2.default(req.params.appId);
      var filesController = config.filesController;
      var filename = req.params.filename;
      var contentType = _mime2.default.lookup(filename);
      if (isFileStreamable(req, filesController)) {
        filesController.getFileStream(config, filename).then(function (stream) {
          handleFileStream(stream, req, res, contentType);
        }).catch(function () {
          res.status(404);
          res.set('Content-Type', 'text/plain');
          res.end('File not found.');
        });
      } else {
        filesController.getFileData(config, filename).then(function (data) {
          res.status(200);
          res.set('Content-Type', contentType);
          res.set('Content-Length', data.length);
          res.end(data);
        }).catch(function () {
          res.status(404);
          res.set('Content-Type', 'text/plain');
          res.end('File not found.');
        });
      }
    }},
    

    Method example for streaming with iOS parse sdk(swift):

    vidObject.video.getDataStreamInBackground(block: <#T##PFDataStreamResultBlock?##PFDataStreamResultBlock?##(InputStream?, Error?) -> Void#>)
    

    I have a local mongodb with parse-server and needed this to make it work:

    Parse-server stream video to IOS from PFFile.url

    Don’t know if it is the same with non-local databases though.