service/components/contents/StoredContent.php
/**
     * Sends content to browser as stream
     *
     * @return mixed
     */
Sends content to browser as stream
@return mixed
Sends content to browser as stream @return mixed
public function streamContent()
    {
        set_time_limit(0);
        ignore_user_abort(false);
        $handler = function ($owner) {
            if (connection_status() == CONNECTION_ABORTED) {
                $afterContentSentEvent = new AfterContentSentEvent();
                $storedContent = $owner->getModel();
                $afterContentSentEvent->bytes_sended = $owner->getBytesStreamed();
                $afterContentSentEvent->content_link = $storedContent->ext_url;
                $afterContentSentEvent->content_name = $storedContent->name;
                $afterContentSentEvent->content_size = $storedContent->size;
                $afterContentSentEvent->user_id = (Yii::$app->user->identity != null) ? Yii::$app->user->identity->id : null;
                $owner->trigger(StreamBehavior::EVENT_AFTER_STREAM, $afterContentSentEvent);
                Yii::$app->statistic->flush();
            }
        };
        register_shutdown_function($handler, $this);
        $this->bytesStreamed = 0;
        $this->trigger(StreamBehavior::EVENT_BEFORE_STREAM);
        $range = $this->getHttpRange($this->length);
        if ($range === false) {
            header_remove();
            header('Content-Range: bytes */'.$this->length);
            throw new HttpException(416, 'Requested range not satisfiable');
        }
        list($start, $end) = $range;
        $isRangeReq = ($start !== null && $end !== null && ($start !== 0 || $end !== $this->length - 1));
        if ($isRangeReq) {
            http_response_code(206);
            header('Content-Range: bytes '.$start.'-'.$end.'/'.$this->length);
            header('Content-Length: '.($end - $start) + 1);
        }
        header('Content-Type: '.$this->mimeType);
        header('Accept-Ranges: bytes');
        if (!ob_get_level()) {
            ob_start();
        }
        $chunks = ($isRangeReq) ? $this->getChunks($start, $end) : $this->getChunks();
        $lastChunkEnd = 0;
        foreach ($chunks as $chunk) {
            $params = [];
            $chunkStart = $chunk->start;
            $chunkEnd = $chunk->end;
            try {
                $content = Yii::$app->storage->getFileContent($chunk->file);
            } catch (\Exception $e) {
                $content = null;
            }
            if ($content === null) {
                Yii::error('Failed to create content for stored content chunk.');
                if (!headers_sent()) {
                    header_remove();
                    throw new HttpException(500, 'Internal server error occured. Please try again later.');
                } else {
                    exit(1);
                }
            }
            if ($isRangeReq) {
                if ($start > $chunk->start && $start <= $chunk->end) {
                    $chunkStart = $start;
                }
                if ($end >= $chunk->start && $end < $chunk->end) {
                    $chunkEnd = $end;
                }
            }
            if ($lastChunkEnd >= $chunkEnd) {
                continue;
            }
            if ($lastChunkEnd > $chunkStart) {
                $chunkStart = $lastChunkEnd + 1;
            }
            if ($chunkStart > $chunk->start || $chunkEnd < $chunk->end) {
                $params['rangeStart'] = ($chunkStart > $chunk->start) ? $chunkStart - $chunk->start : 0;
                $params['rangeEnd'] = ($chunkEnd < $chunk->end) ? $chunk->length - 1 - ($chunk->end - $chunkEnd) : $chunk->length - 1;
            }
            $context = $content->createStreamContext();
            $stream = $content->createStream($context, 'r', $params);
            $contentChunkSentEvent = new ContentChunkSentEvent();
            $contentChunkSentEvent->stream = $stream;
            $contentChunkSentEvent->isPartial = $isRangeReq;
            try {
                while (($data = $stream->read())) {
                    $contentChunkSentEvent->content = $data;
                    $contentChunkSentEvent->bytes = strlen($data);
                    $this->trigger(StreamBehavior::EVENT_BEFORE_CONTENT_CHUNK_SENT, $contentChunkSentEvent);
                    echo $data;
                    ob_flush();
                    flush();
                    $this->trigger(StreamBehavior::EVENT_AFTER_CONTENT_CHUNK_SENT, $contentChunkSentEvent);
                    $this->bytesStreamed += strlen($data);
                }
            } catch (\Exception $e) {
                @ob_end_clean();
                $stream->close();
                Yii::error($e);
                exit(1);
            }
            $stream->close();
            $lastChunkEnd = $chunk->end;
        }
        @ob_end_clean();
        $afterContentSentEvent = new AfterContentSentEvent();
        $storedContent = $this->getModel();
        $afterContentSentEvent->bytes_sended = $this->getBytesStreamed();
        $afterContentSentEvent->content_link = $storedContent->ext_url;
        $afterContentSentEvent->content_name = $storedContent->name;
        $afterContentSentEvent->content_size = $storedContent->size;
        $afterContentSentEvent->user_id = (Yii::$app->user->identity != null) ? Yii::$app->user->identity->id : null;
        $this->trigger(StreamBehavior::EVENT_AFTER_STREAM, $afterContentSentEvent);
    }
public function streamContent ( ) { set_time_limit ( 0 ) ; ignore_user_abort ( false ) ; $ handler = function ( $ owner ) { if ( connection_status ( ) == CONNECTION_ABORTED ) { $ afterContentSentEvent = new AfterContentSentEvent ( ) ; $ storedContent = $ owner -> getModel ( ) ; $ afterContentSentEvent -> bytes_sended = $ owner -> getBytesStreamed ( ) ; $ afterContentSentEvent -> content_link = $ storedContent -> ext_url ; $ afterContentSentEvent -> content_name = $ storedContent -> name ; $ afterContentSentEvent -> content_size = $ storedContent -> size ; $ afterContentSentEvent -> user_id = ( Yii :: $ app -> user -> identity != null ) ? Yii :: $ app -> user -> identity -> id : null ; $ owner -> trigger ( StreamBehavior :: EVENT_AFTER_STREAM , $ afterContentSentEvent ) ; Yii :: $ app -> statistic -> flush ( ) ; } } ; register_shutdown_function ( $ handler , $ this ) ; $ this -> bytesStreamed = 0 ; $ this -> trigger ( StreamBehavior :: EVENT_BEFORE_STREAM ) ; $ range = $ this -> getHttpRange ( $ this -> length ) ; if ( $ range === false ) { header_remove ( ) ; header ( 'Content-Range: bytes */' . $ this -> length ) ; throw new HttpException ( 416 , 'Requested range not satisfiable' ) ; } list ( $ start , $ end ) = $ range ; $ isRangeReq = ( $ start !== null && $ end !== null && ( $ start !== 0 || $ end !== $ this -> length - 1 ) ) ; if ( $ isRangeReq ) { http_response_code ( 206 ) ; header ( 'Content-Range: bytes ' . $ start . '-' . $ end . '/' . $ this -> length ) ; header ( 'Content-Length: ' . ( $ end - $ start ) + 1 ) ; } header ( 'Content-Type: ' . $ this -> mimeType ) ; header ( 'Accept-Ranges: bytes' ) ; if ( ! ob_get_level ( ) ) { ob_start ( ) ; } $ chunks = ( $ isRangeReq ) ? $ this -> getChunks ( $ start , $ end ) : $ this -> getChunks ( ) ; $ lastChunkEnd = 0 ; foreach ( $ chunks as $ chunk ) { $ params = [ ] ; $ chunkStart = $ chunk -> start ; $ chunkEnd = $ chunk -> end ; try { $ content = Yii :: $ app -> storage -> getFileContent ( $ chunk -> file ) ; } catch ( \ Exception $ e ) { $ content = null ; } if ( $ content === null ) { Yii :: error ( 'Failed to create content for stored content chunk.' ) ; if ( ! headers_sent ( ) ) { header_remove ( ) ; throw new HttpException ( 500 , 'Internal server error occured. Please try again later.' ) ; } else { exit ( 1 ) ; } } if ( $ isRangeReq ) { if ( $ start > $ chunk -> start && $ start <= $ chunk -> end ) { $ chunkStart = $ start ; } if ( $ end >= $ chunk -> start && $ end < $ chunk -> end ) { $ chunkEnd = $ end ; } } if ( $ lastChunkEnd >= $ chunkEnd ) { continue ; } if ( $ lastChunkEnd > $ chunkStart ) { $ chunkStart = $ lastChunkEnd + 1 ; } if ( $ chunkStart > $ chunk -> start || $ chunkEnd < $ chunk -> end ) { $ params [ 'rangeStart' ] = ( $ chunkStart > $ chunk -> start ) ? $ chunkStart - $ chunk -> start : 0 ; $ params [ 'rangeEnd' ] = ( $ chunkEnd < $ chunk -> end ) ? $ chunk -> length - 1 - ( $ chunk -> end - $ chunkEnd ) : $ chunk -> length - 1 ; } $ context = $ content -> createStreamContext ( ) ; $ stream = $ content -> createStream ( $ context , 'r' , $ params ) ; $ contentChunkSentEvent = new ContentChunkSentEvent ( ) ; $ contentChunkSentEvent -> stream = $ stream ; $ contentChunkSentEvent -> isPartial = $ isRangeReq ; try { while ( ( $ data = $ stream -> read ( ) ) ) { $ contentChunkSentEvent -> content = $ data ; $ contentChunkSentEvent -> bytes = strlen ( $ data ) ; $ this -> trigger ( StreamBehavior :: EVENT_BEFORE_CONTENT_CHUNK_SENT , $ contentChunkSentEvent ) ; echo $ data ; ob_flush ( ) ; flush ( ) ; $ this -> trigger ( StreamBehavior :: EVENT_AFTER_CONTENT_CHUNK_SENT , $ contentChunkSentEvent ) ; $ this -> bytesStreamed += strlen ( $ data ) ; } } catch ( \ Exception $ e ) { @ ob_end_clean ( ) ; $ stream -> close ( ) ; Yii :: error ( $ e ) ; exit ( 1 ) ; } $ stream -> close ( ) ; $ lastChunkEnd = $ chunk -> end ; } @ ob_end_clean ( ) ; $ afterContentSentEvent = new AfterContentSentEvent ( ) ; $ storedContent = $ this -> getModel ( ) ; $ afterContentSentEvent -> bytes_sended = $ this -> getBytesStreamed ( ) ; $ afterContentSentEvent -> content_link = $ storedContent -> ext_url ; $ afterContentSentEvent -> content_name = $ storedContent -> name ; $ afterContentSentEvent -> content_size = $ storedContent -> size ; $ afterContentSentEvent -> user_id = ( Yii :: $ app -> user -> identity != null ) ? Yii :: $ app -> user -> identity -> id : null ; $ this -> trigger ( StreamBehavior :: EVENT_AFTER_STREAM , $ afterContentSentEvent ) ; }
Sends content to browser as stream
 @return mixed
Sends content to browser as stream @return mixed
//This should be executed only when connection is aborted. //We must flush statistics manualy. //Register shutdown function to handle connection abort //Trigger before download event //Get stored content chunks //Last sended chunk's end //If request is range request and chunk contains start\end set start\end position of chunk //Since chunks can overlap need to check if chunk content was not already sended //If part of current chunk was already sent - update chunk's start position //Trigger before content chunk sent event //Trigger after content chunk sent event //Trigger after download event