2

I have an ios app with laravel (lumen) on the server side. I am trying to play the videos in the server, on application.

I am using a Player that plays videos with a direct link (e.g vine video link), however when I save the same vine video on my local server, the application doesn't play the video. In fact, when I try the video with my api route, surprisingly it plays the video on Chrome! But on the application end, I receive error:

The server is not correctly configured - 12939

(Please note that if I copy the same mp4 file into the xCode project, add it on 'copy bundle resources', and try with fileWithPath, it works. So I believe it's definitely caused by the server, not vidoo file/codec. )

My route: $app->get('/player/{filename}', 'PlayerController@show');

Methods:

public function show ($filename)
{
  $this->playVideo($filename, 'recordings');
}

public function playVideo($filename, $showType)
{
    if (file_exists("../uploads/" . $showType . "/" . $filename)) {
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $type = finfo_file($finfo, "../uploads/" . $showType . "/" . $filename);


        header("Content-Type: " . $type);
        readfile("../uploads/" . $showType . "/" . $filename);
    }
}

To recap my problem, the video is playing on Chrome but receiving '12939' - 'Server is not correctly configured' on the mobile app.

Edit:

I tried using this as mentioned in the Apple Documentations:

curl --range 0-99 http://myapi.dev/test.mp4 -o /dev/null

However the documentation says:

"If the tool reports that it downloaded 100 bytes, the media server correctly handled the byte-range request. If it downloads the entire file, you may need to update the media server."

I received 100% and it downloaded the whole file for me, so I believe this is my problem. But I am not sure how to overcome this issue? What am I doing wrong? What should I do?

senty
  • 12,385
  • 28
  • 130
  • 260
  • Probably a range request issue. Check this thread: http://stackoverflow.com/questions/3397241/does-iphone-ipad-safari-require-accept-ranges-header-for-video – Joel Hinz Dec 20 '15 at 23:27
  • Mine comes to 100% and completes. It doesn't stop at any point, it just seems that it downloads whole file (even though I can't see any file created at this path). Does that mean, this is my problem? :/ I am totally confused at this point. **Edit:** Just realised that whatever file name I put at the end of the route is receiving 100% with the code [`curl --range 0-99 http://example.com/video.mp4 -o /dev/null`], even if the file with that filename doesn't exist! – senty Dec 20 '15 at 23:40

2 Answers2

8

To wrap up, this solved my problem:

  • Placed this to VideoController:

    public function streamVideo() 
    {
      $video_path = 'somedirectory/somefile.mp4';
      $stream = new VideoStream($video_path);
      $stream->start(); 
    }
    
  • and then created a file in app > helpers > VideoStream.php:

     <?php
     {
         private $path = "";
         private $stream = "";
         private $buffer = 102400;
         private $start  = -1;
         private $end    = -1;
         private $size   = 0;
    
         function __construct($filePath) 
         {
             $this->path = $filePath;
         }
    
         /**
          *      * Open stream
          *           */
         private function open()
         {
             if (!($this->stream = fopen($this->path, 'rb'))) {
                 die('Could not open stream for reading');
             }
    
         }
    
         /**
          *      * Set proper header to serve the video content
          *           */
         private function setHeader()
         {
             ob_get_clean();
             header("Content-Type: video/mp4");
             header("Cache-Control: max-age=2592000, public");
             header("Expires: ".gmdate('D, d M Y H:i:s', time()+2592000) . ' GMT');
             header("Last-Modified: ".gmdate('D, d M Y H:i:s', @filemtime($this->path)) . ' GMT' );
             $this->start = 0;
             $this->size  = filesize($this->path);
             $this->end   = $this->size - 1;
             header("Accept-Ranges: 0-".$this->end);
    
             if (isset($_SERVER['HTTP_RANGE'])) {
    
                 $c_start = $this->start;
                 $c_end = $this->end;
    
                 list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
                 if (strpos($range, ',') !== false) {
                     header('HTTP/1.1 416 Requested Range Not Satisfiable');
                     header("Content-Range: bytes $this->start-$this->end/$this->size");
                     exit;
                 }
                 if ($range == '-') {
                     $c_start = $this->size - substr($range, 1);
                 }else{
                     $range = explode('-', $range);
                     $c_start = $range[0];
    
                     $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $c_end;
                 }
                 $c_end = ($c_end > $this->end) ? $this->end : $c_end;
                 if ($c_start > $c_end || $c_start > $this->size - 1 || $c_end >= $this->size) {
                     header('HTTP/1.1 416 Requested Range Not Satisfiable');
                     header("Content-Range: bytes $this->start-$this->end/$this->size");
                     exit;
                 }
                 $this->start = $c_start;
                 $this->end = $c_end;
                 $length = $this->end - $this->start + 1;
                 fseek($this->stream, $this->start);
                 header('HTTP/1.1 206 Partial Content');
                 header("Content-Length: ".$length);
                 header("Content-Range: bytes $this->start-$this->end/".$this->size);
             }
             else
             {
                 header("Content-Length: ".$this->size);
             }  
    
         }
    
         /**
          *      * close curretly opened stream
          *           */
         private function end()
         {
             fclose($this->stream);
             exit;
         }
    
         /**
          *      * perform the streaming of calculated range
          *           */
         private function stream()
         {
             $i = $this->start;
             set_time_limit(0);
             while(!feof($this->stream) && $i <= $this->end) {
                 $bytesToRead = $this->buffer;
                 if(($i+$bytesToRead) > $this->end) {
                     $bytesToRead = $this->end - $i + 1;
                 }
                 $data = fread($this->stream, $bytesToRead);
                 echo $data;
                 flush();
                 $i += $bytesToRead;
             }
         }
    
         /**
          *      * Start streaming video content
          *           */
         function start()
         {
             $this->open();
             $this->setHeader();
             $this->stream();
             $this->end();
         }
     }
    

Source: http://laravel.io/forum/10-06-2014-streaming-video-files-with-laravel

senty
  • 12,385
  • 28
  • 130
  • 260
2

I ran into a problem similar to this one, but your case is going to take a bit more configuration.

The headers for iOS need to be set appropriately using range requests, and the only way I was able to do that for all browsers was by reworking this gist to what I needed.

In my case, I did something like this in Laravel:

//Video controller
...
public function showVideo($id){
    $video = Video::find($id);
    return $this->videoService->stream($video);
}

//Video Service
namespace App\Services\VideoService;
use Illuminate\Routing\ResponseFactory as Response;
use App\Models\Video;
class VideoService implements VideoServiceInterface
{
    protected $response;
    protected $video;
    private $stream = "";
    private $buffer = 102400;
    private $start  = -1;
    private $end    = -1;
    private $size   = 0;
    public function __construct(Response $response){
        $this->response = $response;
    }
    public function stream(Video $video){
        $this->video = $video;
        return $this->response->stream(function(){
            $this->start();
        });
    }
    //Implement the rest of the gist here, renaming where appropriate....
}

Problem is, you're using Lumen which does not support a stream method on the response factory. What you'll need to do is rework the code I gave you to include Symfony's StreamedResponse Object.

If you look at how Laravel does it, you can probably do something like this:

//Video Service
namespace App\Services\VideoService;
use Symfony\Component\HttpFoundation\StreamedResponse as Response;
use App\Models\Video;
class VideoService implements VideoServiceInterface
{
    protected $video;
    private $stream = "";
    private $buffer = 102400;
    private $start  = -1;
    private $end    = -1;
    private $size   = 0;

    public function stream(Video $video){
        $this->video = $video;
        return new Response(function(){
            $this->start();
        });
    }
    //Implement the rest of the gist here, renaming where appropriate....
}

This isn't exact, and it won't work for you out of the box. But this should give you all of the components you need to register your own Service Provider and apply this appropriately to your own use-case.

Good luck.

maiorano84
  • 11,574
  • 3
  • 35
  • 48