├── .gitignore ├── src ├── PushServer │ ├── VerifyAuthStreamInterface.php │ ├── DuplexMediaStreamInterface.php │ ├── PublishStreamInterface.php │ └── PlayStreamInterface.php ├── Rtmp │ ├── RtmpAuthorizeTrait.php │ ├── RtmpEventHandlerTrait.php │ ├── RtmpChunk.php │ ├── RtmpControlHandlerTrait.php │ ├── RtmpHandshake.php │ ├── RtmpDataHandlerTrait.php │ ├── RtmpHandshakeTrait.php │ ├── RtmpAudioHandlerTrait.php │ ├── RtmpPublisherTrait.php │ ├── RtmpPacket.php │ ├── RtmpVideoHandlerTrait.php │ ├── RtmpTrait.php │ ├── RtmpPacketTrait.php │ ├── RtmpPlayerTrait.php │ ├── RtmpAMF.php │ ├── RtmpStream.php │ └── RtmpChunkHandlerTrait.php ├── Flv │ ├── FlvTag.php │ ├── FlvHeader.php │ ├── Flv.php │ └── FlvPlayStream.php ├── MediaReader │ ├── MediaFrame.php │ ├── MetaDataFrame.php │ ├── AVCPacket.php │ ├── AACPacket.php │ ├── VideoFrame.php │ ├── AACSequenceParameterSet.php │ ├── AudioFrame.php │ └── AVCSequenceParameterSet.php ├── Utils │ ├── WMChunkStreamInterface.php │ ├── BitReader.php │ ├── WMWsChunkStream.php │ ├── WMHttpChunkStream.php │ ├── WMBufferStream.php │ └── BinaryStream.php ├── functions.php ├── Http │ ├── ExtHttpProtocol.php │ └── HttpWMServer.php └── MediaServer.php ├── packages └── SabreAMF │ ├── ChangeLog │ ├── README │ ├── DetailException.php │ ├── Externalized.php │ ├── AMF3 │ ├── Const.php │ ├── Wrapper.php │ ├── RemotingMessage.php │ ├── AcknowledgeMessage.php │ ├── CommandMessage.php │ ├── AbstractMessage.php │ └── ErrorMessage.php │ ├── AMF0 │ ├── Const.php │ └── Deserializer.php │ ├── ITypedObject.php │ ├── InvalidAMFException.php │ ├── ByteArray.php │ ├── ClassNotFoundException.php │ ├── UndefinedMethodException.php │ ├── LICENCE │ ├── Deserializer.php │ ├── TypedObject.php │ ├── RecordSet.php │ ├── Const.php │ ├── Serializer.php │ ├── OutputStream.php │ ├── InputStream.php │ ├── ArrayCollection.php │ ├── ClassMapper.php │ ├── Client.php │ ├── Server.php │ ├── Message.php │ └── CallbackServer.php ├── composer.json ├── start.php ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | composer.lock 3 | /vendor/ 4 | -------------------------------------------------------------------------------- /src/PushServer/VerifyAuthStreamInterface.php: -------------------------------------------------------------------------------- 1 | info("rtmpEventHandler"); 12 | } 13 | } -------------------------------------------------------------------------------- /src/MediaReader/MediaFrame.php: -------------------------------------------------------------------------------- 1 | dump(); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /packages/SabreAMF/ChangeLog: -------------------------------------------------------------------------------- 1 | SabreAMF 1.3 - 2009-06-27 2 | * Fixed: Issue 6: Invalid command operation 12 3 | * Fixed: AMF3 deserialization improvements on 64bit machines and various 4 | other bugs (thanks Asbjorn) 5 | * Fixed: Issue 11: AMF0 array reference bugs fixed 6 | * Fixed: AMF0 classmapping bugs 7 | * Fixed: Bug in recordset object notation 8 | * New: If invalid AMF is received, we send back a more specific 9 | exception (thanks Asbjorn) 10 | * New: Drupal integration module (separate download) (thanks Ionut) 11 | 12 | Older: 13 | * Changelogs to follow, if anyone is interested 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wy3/media-server", 3 | "type": "project", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "wy3", 8 | "email": "908005070@qq.com" 9 | } 10 | ], 11 | "require": { 12 | "php": "^7.4", 13 | "ext-json": "*", 14 | "evenement/evenement": "^3.0", 15 | "apix/log": "^1.2", 16 | "workerman/workerman": "^4.1", 17 | "react/promise": "^2.9" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "MediaServer\\": "src" 22 | } 23 | }, 24 | "files": [ 25 | "src/functions.php" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/Flv/FlvHeader.php: -------------------------------------------------------------------------------- 1 | signature = $data['signature']; 21 | $this->version = $data['version']; 22 | $this->typeFlags = $data['typeFlags']; 23 | $this->dataOffset = $data['dataOffset']; 24 | $this->hasAudio = $this->typeFlags & 4 ? true : false; 25 | $this->hasVideo = $this->typeFlags & 1 ? true : false; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/SabreAMF/README: -------------------------------------------------------------------------------- 1 | SabreAMF v1.3 2 | 3 | Copyright (C) 2006-2009 Rooftop Solutions. All rights reserved. 4 | 5 | Documentation, bug reports, downloads: 6 | http://code.google.com/p/sabreamf/ 7 | 8 | SabreAMF is distributed under a BSD licence. Please check the LICENCE file for more information. 9 | 10 | Main Author: 11 | Evert Pot (http://www.rooftopsolutions.nl) 12 | 13 | Bugfixes, suggestions, improvements: 14 | Karl von Randow (http://xk72.com/blog) 15 | Renaun Erickson (http://renaun.com/blog) 16 | Asbjørn Sloth Tønnesen (http://lila.io/) 17 | 18 | Drupal integration: 19 | Ionut Stoica 20 | 21 | Thanks to: 22 | Kevin Langdon, author of ServiceCapture for making the first big steps in decoding the AMF3 spec and helping out when i got stuck 23 | The AMFPHP team for their excellent piece of software. 24 | The OSFlash community for opening up the Flash Platform. 25 | PyAMF project 26 | -------------------------------------------------------------------------------- /packages/SabreAMF/DetailException.php: -------------------------------------------------------------------------------- 1 | onConnect = function (\Workerman\Connection\TcpConnection $connection) { 9 | logger()->info("connection" . $connection->getRemoteAddress() . " connected . "); 10 | new \MediaServer\Rtmp\RtmpStream( 11 | new \MediaServer\Utils\WMBufferStream($connection) 12 | ); 13 | }; 14 | $rtmpServer->onWorkerStart = function ($worker) { 15 | logger()->info("rtmp server " . $worker->getSocketName() . " start . "); 16 | \MediaServer\Http\HttpWMServer::$publicPath = __DIR__.'/public'; 17 | $httpServer = new \MediaServer\Http\HttpWMServer("\\MediaServer\\Http\\ExtHttpProtocol://127.0.0.1:18080"); 18 | $httpServer->listen(); 19 | logger()->info("http server " . $httpServer->getSocketName() . " start . "); 20 | }; 21 | 22 | \Workerman\Worker::runAll(); 23 | 24 | -------------------------------------------------------------------------------- /packages/SabreAMF/Externalized.php: -------------------------------------------------------------------------------- 1 | currentPacket; 13 | switch ($p->type) { 14 | case RtmpPacket::TYPE_SET_CHUNK_SIZE: 15 | list(, $this->inChunkSize) = unpack("N", $p->payload); 16 | logger()->debug('set inChunkSize ' . $this->inChunkSize); 17 | break; 18 | case RtmpPacket::TYPE_ABORT: 19 | break; 20 | case RtmpPacket::TYPE_ACKNOWLEDGEMENT: 21 | break; 22 | case RtmpPacket::TYPE_WINDOW_ACKNOWLEDGEMENT_SIZE: 23 | list(, $this->ackSize) = unpack("N", $p->payload); 24 | logger()->debug('set ack Size ' . $this->ackSize); 25 | break; 26 | case RtmpPacket::TYPE_SET_PEER_BANDWIDTH: 27 | break; 28 | } 29 | 30 | //logger()->info("rtmpControlHandler use:" . ((microtime(true) - $b) * 1000) . 'ms'); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Rtmp/RtmpHandshake.php: -------------------------------------------------------------------------------- 1 | data=$data; 17 | } 18 | 19 | /** 20 | * @param int $bits 21 | */ 22 | public function skipBits($bits) { 23 | $newBits = $this->currentBits + $bits; 24 | $this->currentBytes += (int)floor($newBits / 8); 25 | $this->currentBits = $newBits % 8; 26 | } 27 | 28 | /** 29 | * @return int 30 | */ 31 | public function getBit() { 32 | if(!isset($this->data[$this->currentBytes])){ 33 | $this->isError=true; 34 | return 0; 35 | } 36 | $result = (ord($this->data[$this->currentBytes]) >> (7 - $this->currentBits)) & 0x01; 37 | $this->skipBits(1); 38 | return $result; 39 | } 40 | 41 | public function getBits($bits){ 42 | $result = 0; 43 | for ($i = 0; $i < $bits; $i++) { 44 | $result = ($result << 1) + $this->getBit(); 45 | } 46 | return $result; 47 | } 48 | 49 | 50 | 51 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP-Media-Server 2 | 3 | ## 说明 4 | 5 | > 本项目为基于workerman实现的纯PHP流媒体服务 6 | 7 | 目前实现的功能: 8 | 9 | rtmp(flv)推流 10 | 11 | rtmp,httpflv,wsflv播放 12 | 13 | 14 | 需要注意的是目前功能并未完善, 可能会遇到但不仅限于以下问题: 15 | 16 | 1. 视频流加载缓慢 17 | 18 | 2. 部分播放器可能播放有问题 19 | 20 | 3. 不支持多进程 21 | 22 | 4. 非 h264/aac 流支持可能存在问题 23 | 24 | 5. 可能存在内存溢出,特定环境下可能会报错退出 25 | 26 | ... 27 | 28 | > 另外Node-Media-Server, sabreamf 等开源项目为本项目中协议的解析实现提供了极大的帮助 29 | 30 | ## 安装 31 | 32 | ```php 7.4``` 33 | 34 | ```composer install``` 35 | 36 | ## 启动 37 | 38 | ```` 39 | php start.php start 40 | ```` 41 | 42 | ## ffmpeg推流 43 | 44 | ``` 45 | ffmpeg.exe -re -stream_loop 1 -i "file.mp4" -vcodec h264 -acodec aac -f flv rtmp://127.0.0.1/a/b 46 | ``` 47 | 48 | ## 播放 49 | 50 | rtmp播放地址: ``rtmp://127.0.0.1/a/b`` 51 | 52 | httpflv播放地址: ``http://127.0.0.1:18080/a/b.flv`` 53 | 54 | wsflv播放地址: ``ws://127.0.0.1:18080/a/b.flv`` 55 | 56 | web播放sdk: [Aliplayer](http://player.alicdn.com/aliplayer/setting/setting.html) 57 | 58 | 客户端播放器: ``vlc``,``ffplay`` 59 | 60 | 61 | 62 | ## 致谢 63 | 64 | https://www.workerman.net 65 | 66 | https://github.com/illuspas/Node-Media-Server 67 | 68 | https://code.google.com/archive/p/sabreamf/ 69 | 70 | -------------------------------------------------------------------------------- /packages/SabreAMF/AMF0/Const.php: -------------------------------------------------------------------------------- 1 | stream=$stream; 29 | $this->avcPacketType=$stream->readTinyInt(); 30 | $this->compositionTime=$stream->readInt24(); 31 | } 32 | 33 | 34 | /** 35 | * @var AACSequenceParameterSet 36 | */ 37 | protected $avcSequenceParameterSet; 38 | 39 | /** 40 | * @return AVCSequenceParameterSet 41 | */ 42 | public function getAVCSequenceParameterSet(){ 43 | 44 | if(!$this->avcSequenceParameterSet){ 45 | $this->avcSequenceParameterSet=new AVCSequenceParameterSet($this->stream->readRaw()); 46 | } 47 | return $this->avcSequenceParameterSet; 48 | } 49 | } -------------------------------------------------------------------------------- /packages/SabreAMF/ITypedObject.php: -------------------------------------------------------------------------------- 1 | 10 | * @licence http://www.freebsd.org/copyright/license.html BSD License (4 Clause) 11 | */ 12 | 13 | /** 14 | * Detailed exception 15 | */ 16 | require_once dirname(__FILE__) . '/DetailException.php'; 17 | 18 | /** 19 | * In valid AMF exception 20 | * 21 | * @uses SabreAMF_DetailException 22 | */ 23 | class SabreAMF_InvalidAMFException extends Exception implements SabreAMF_DetailException { 24 | 25 | /** 26 | * Constructor 27 | */ 28 | public function __construct() { 29 | // Specific message to ClassException 30 | $this->message = "No valid AMF request received"; 31 | $this->code = "Server.Processing"; 32 | 33 | // Call parent class constructor 34 | parent::__construct( $this->message ); 35 | } 36 | 37 | public function getDetail() { 38 | 39 | return "Please check that you are calling this page with Flash and AMF."; 40 | 41 | } 42 | 43 | } 44 | 45 | ?> 46 | -------------------------------------------------------------------------------- /packages/SabreAMF/ByteArray.php: -------------------------------------------------------------------------------- 1 | data = $data; 30 | 31 | } 32 | 33 | /** 34 | * getData 35 | * 36 | * @return string 37 | */ 38 | function getData() { 39 | 40 | return $this->data; 41 | 42 | } 43 | 44 | /** 45 | * setData 46 | * 47 | * @param string $data 48 | * @return void 49 | */ 50 | function setData($data) { 51 | 52 | $this->data = $data; 53 | 54 | } 55 | 56 | } 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/Utils/WMWsChunkStream.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 25 | $this->connection->onClose = function ($con){ 26 | $this->emit('close'); 27 | $this->connection = null; 28 | $this->removeAllListeners(); 29 | }; 30 | $this->connection->onError = function ($con,$code,$msg){ 31 | $this->emit('error',[new \Exception($msg,$code)]); 32 | }; 33 | } 34 | 35 | 36 | public function write($data) 37 | { 38 | $this->connection->send($data); 39 | } 40 | 41 | public function end($data = null) 42 | { 43 | //empty chunk end 44 | $this->connection->send(new Chunk('')); 45 | } 46 | 47 | public function close() 48 | { 49 | $this->connection->close(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/SabreAMF/AMF3/Wrapper.php: -------------------------------------------------------------------------------- 1 | setData($data); 33 | 34 | } 35 | 36 | 37 | /** 38 | * getData 39 | * 40 | * @return mixed 41 | */ 42 | public function getData() { 43 | 44 | return $this->data; 45 | 46 | } 47 | 48 | /** 49 | * setData 50 | * 51 | * @param mixed $data 52 | * @return void 53 | */ 54 | public function setData($data) { 55 | 56 | $this->data = $data; 57 | 58 | } 59 | 60 | 61 | } 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/MediaReader/AACPacket.php: -------------------------------------------------------------------------------- 1 | stream=$stream; 42 | $this->aacPacketType=$stream->readTinyInt(); 43 | 44 | } 45 | 46 | /** 47 | * @var AACSequenceParameterSet 48 | */ 49 | protected $aacSequenceParameterSet; 50 | 51 | /** 52 | * @return AACSequenceParameterSet 53 | */ 54 | public function getAACSequenceParameterSet(){ 55 | 56 | if(!$this->aacSequenceParameterSet){ 57 | $this->aacSequenceParameterSet=new AACSequenceParameterSet($this->stream->readRaw()); 58 | } 59 | return $this->aacSequenceParameterSet; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /packages/SabreAMF/ClassNotFoundException.php: -------------------------------------------------------------------------------- 1 | message = "Could not locate class " . $classname; 32 | $this->code = "Server.Processing"; 33 | 34 | // Call parent class constructor 35 | parent::__construct( $this->message ); 36 | } 37 | 38 | public function getDetail() { 39 | 40 | return "Please check that the given servicename is correct and that the class exists."; 41 | 42 | } 43 | 44 | } 45 | 46 | ?> 47 | -------------------------------------------------------------------------------- /packages/SabreAMF/UndefinedMethodException.php: -------------------------------------------------------------------------------- 1 | message = "Undefined method '$method' in class $class"; 32 | $this->code = "Server.Processing"; 33 | 34 | // Call parent class constructor 35 | parent::__construct( $this->message ); 36 | 37 | } 38 | 39 | public function getDetail() { 40 | 41 | return "Check to ensure that the method is defined, and that it is spelled correctly."; 42 | 43 | } 44 | 45 | 46 | } 47 | 48 | ?> 49 | -------------------------------------------------------------------------------- /packages/SabreAMF/AMF3/RemotingMessage.php: -------------------------------------------------------------------------------- 1 | messageId = $this->generateRandomId(); 44 | $this->clientId = $this->generateRandomId(); 45 | $this->destination = null; 46 | $this->body = null; 47 | $this->timeToLive = 0; 48 | $this->timestamp = time() . '00'; 49 | $this->headers = new STDClass(); 50 | 51 | } 52 | } 53 | 54 | 55 | -------------------------------------------------------------------------------- /packages/SabreAMF/AMF3/AcknowledgeMessage.php: -------------------------------------------------------------------------------- 1 | messageId = $this->generateRandomId(); 35 | $this->clientId = $this->generateRandomId(); 36 | $this->destination = null; 37 | $this->body = null; 38 | $this->timeToLive = 0; 39 | $this->timestamp = time() . '00'; 40 | $this->headers = new STDClass(); 41 | 42 | if ($message) { 43 | $this->correlationId = $message->messageId; 44 | } 45 | 46 | } 47 | 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /packages/SabreAMF/LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2006-2009 Rooftop Solutions. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of the SabreAMF nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 20 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /src/PushServer/PublishStreamInterface.php: -------------------------------------------------------------------------------- 1 | currentPacket; 19 | //AMF0 数据解释 20 | $dataMessage = RtmpAMF::rtmpDataAmf0Reader($p->payload); 21 | logger()->info("rtmpDataHandler {$dataMessage['cmd']} " . json_encode($dataMessage)); 22 | switch ($dataMessage['cmd']) { 23 | case '@setDataFrame': 24 | if (isset($dataMessage['dataObj'])) { 25 | $this->audioSamplerate = $dataMessage['dataObj']['audiosamplerate'] ?? $this->audioSamplerate; 26 | $this->audioChannels = isset($dataMessage['dataObj']['stereo']) ? ($dataMessage['dataObj']['stereo'] ? 2 : 1) : $this->audioChannels; 27 | $this->videoWidth = $dataMessage['dataObj']['width'] ?? $this->videoWidth; 28 | $this->videoHeight = $dataMessage['dataObj']['height'] ?? $this->videoHeight; 29 | $this->videoFps = $dataMessage['dataObj']['framerate'] ?? $this->videoFps; 30 | } 31 | 32 | $this->isMetaData = true; 33 | $metaDataFrame = new MetaDataFrame(RtmpAMF::rtmpDATAAmf0Creator([ 34 | 'cmd' => 'onMetaData', 35 | 'dataObj' => $dataMessage['dataObj'] 36 | ])); 37 | $this->metaDataFrame = $metaDataFrame; 38 | 39 | $this->emit('on_frame', [$metaDataFrame, $this]); 40 | 41 | //播放类群发onMetaData 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /packages/SabreAMF/AMF3/CommandMessage.php: -------------------------------------------------------------------------------- 1 | stream = $stream; 40 | 41 | } 42 | 43 | /** 44 | * readAMFData 45 | * 46 | * Starts reading an AMF block from the stream 47 | * 48 | * @param mixed $settype 49 | * @return mixed 50 | */ 51 | public abstract function readAMFData($settype = null); 52 | 53 | 54 | /** 55 | * getLocalClassName 56 | * 57 | * @param string $remoteClass 58 | * @return mixed 59 | */ 60 | protected function getLocalClassName($remoteClass) { 61 | 62 | return SabreAMF_ClassMapper::getLocalClass($remoteClass); 63 | 64 | } 65 | 66 | } 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/Utils/WMHttpChunkStream.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 27 | $this->connection->onClose = function ($con){ 28 | $this->emit('close'); 29 | $this->connection = null; 30 | $this->removeAllListeners(); 31 | }; 32 | $this->connection->onError = function ($con,$code,$msg){ 33 | $this->emit('error',[new \Exception($msg,$code)]); 34 | }; 35 | } 36 | 37 | 38 | public function write($data) 39 | { 40 | if(!$this->sendHeader){ 41 | $this->sendHeader = true; 42 | $this->connection->send(new Response(200,[ 43 | 'Cache-Control' => 'no-cache', 44 | 'Content-Type' => 'video/x-flv', 45 | 'Access-Control-Allow-Origin' => '*', 46 | 'Connection' => 'keep-alive', 47 | 'Transfer-Encoding' => 'chunked' 48 | ],$data)); 49 | }else{ 50 | $this->connection->send(new Chunk($data)); 51 | } 52 | 53 | } 54 | 55 | public function end($data = null) 56 | { 57 | //empty chunk end 58 | $this->connection->send(new Chunk('')); 59 | } 60 | 61 | public function close() 62 | { 63 | $this->connection->close(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/SabreAMF/AMF3/ErrorMessage.php: -------------------------------------------------------------------------------- 1 | setAMFClassName($classname); 23 | $this->setAMFData($data); 24 | 25 | } 26 | 27 | /** 28 | * getAMFClassName 29 | * 30 | * @return string 31 | */ 32 | public function getAMFClassName() { 33 | 34 | return $this->amfClassName; 35 | 36 | } 37 | 38 | /** 39 | * getAMFData 40 | * 41 | * @return mixed 42 | */ 43 | public function getAMFData() { 44 | 45 | return $this->amfData; 46 | 47 | } 48 | 49 | /** 50 | * setAMFClassName 51 | * 52 | * @param string $classname 53 | * @return void 54 | */ 55 | public function setAMFClassName($classname) { 56 | 57 | $this->amfClassName = $classname; 58 | 59 | } 60 | 61 | /** 62 | * setAMFData 63 | * 64 | * @param mixed $data 65 | * @return void 66 | */ 67 | public function setAMFData($data) { 68 | 69 | $this->amfData = $data; 70 | 71 | } 72 | 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /packages/SabreAMF/RecordSet.php: -------------------------------------------------------------------------------- 1 | (object)array( 53 | 'totalCount' => $this->count(), 54 | 'initialData' => $this->getData(), 55 | 'cursor' => 1, 56 | 'serviceName' => false, 57 | 'columnNames' => $this->getColumnNames(), 58 | 'version' => 1, 59 | 'id' => false, 60 | ) 61 | ); 62 | 63 | 64 | } 65 | 66 | } 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /packages/SabreAMF/Const.php: -------------------------------------------------------------------------------- 1 | buffer; 24 | 25 | switch ($this->handshakeState) { 26 | case RtmpHandshake::RTMP_HANDSHAKE_UNINIT: 27 | if ($stream->has(1)) { 28 | logger()->info('RTMP_HANDSHAKE_UNINIT'); 29 | //read c0 30 | $stream->readByte(); 31 | // goto c0 32 | $this->handshakeState = RtmpHandshake::RTMP_HANDSHAKE_C0; 33 | } else { 34 | break; 35 | } 36 | case RtmpHandshake::RTMP_HANDSHAKE_C0: 37 | if ($stream->has(1536)) { 38 | logger()->info('RTMP_HANDSHAKE_C0'); 39 | $c1=$stream->readRaw(1536); 40 | 41 | //向客户端发送 s0s1s2 42 | $s0s1s2 = RtmpHandshake::handshakeGenerateS0S1S2($c1); 43 | $this->write($s0s1s2); 44 | $this->handshakeState = RtmpHandshake::RTMP_HANDSHAKE_C1; 45 | } else { 46 | break; 47 | } 48 | case RtmpHandshake::RTMP_HANDSHAKE_C1: 49 | if ($stream->has(1536)) { 50 | logger()->info('RTMP_HANDSHAKE_C1'); 51 | $stream->readRaw(1536); 52 | $this->handshakeState = RtmpHandshake::RTMP_HANDSHAKE_C2; 53 | $this->chunkState = RtmpChunk::CHUNK_STATE_BEGIN; 54 | } else { 55 | break; 56 | } 57 | case RtmpHandshake::RTMP_HANDSHAKE_C2: 58 | } 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/SabreAMF/Serializer.php: -------------------------------------------------------------------------------- 1 | stream = $stream; 40 | 41 | } 42 | 43 | /** 44 | * writeAMFData 45 | * 46 | * @param mixed $data 47 | * @param int $forcetype 48 | * @return mixed 49 | */ 50 | public abstract function writeAMFData($data,$forcetype=null); 51 | 52 | /** 53 | * getStream 54 | * 55 | * @return SabreAMF_OutputStream 56 | */ 57 | public function getStream() { 58 | 59 | return $this->stream; 60 | 61 | } 62 | 63 | /** 64 | * getRemoteClassName 65 | * 66 | * @param string $localClass 67 | * @return mixed 68 | */ 69 | protected function getRemoteClassName($localClass) { 70 | 71 | return SabreAMF_ClassMapper::getRemoteClass($localClass); 72 | 73 | } 74 | 75 | } 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/Rtmp/RtmpAudioHandlerTrait.php: -------------------------------------------------------------------------------- 1 | currentPacket; 20 | $audioFrame = new AudioFrame($p->payload, $p->clock); 21 | 22 | 23 | if ($this->audioCodec == 0) { 24 | $this->audioCodec = $audioFrame->soundFormat; 25 | $this->audioCodecName = $audioFrame->getAudioCodecName(); 26 | $this->audioSamplerate = $audioFrame->getAudioSamplerate(); 27 | $this->audioChannels = ++$audioFrame->soundType; 28 | } 29 | 30 | 31 | if ($audioFrame->soundFormat == AudioFrame::SOUND_FORMAT_AAC) { 32 | $aacPack = $audioFrame->getAACPacket(); 33 | if ($aacPack->aacPacketType === AACPacket::AAC_PACKET_TYPE_SEQUENCE_HEADER) { 34 | $this->isAACSequence = true; 35 | $this->aacSequenceHeaderFrame = $audioFrame; 36 | $set = $aacPack->getAACSequenceParameterSet(); 37 | $this->audioProfileName = $set->getAACProfileName(); 38 | $this->audioSamplerate = $set->sampleRate; 39 | $this->audioChannels = $set->channels; 40 | //logger()->info("publisher {path} recv acc sequence.", ['path' => $this->pathIndex]); 41 | } 42 | 43 | if ($this->isAACSequence) { 44 | if ($aacPack->aacPacketType == AACPacket::AAC_PACKET_TYPE_SEQUENCE_HEADER) { 45 | 46 | } else { 47 | //音频关键帧缓存 48 | $this->gopCacheQueue[] = $audioFrame; 49 | } 50 | } 51 | 52 | 53 | } 54 | 55 | 56 | $this->emit('on_frame', [$audioFrame, $this]); 57 | 58 | //logger()->info("rtmpAudioHandler"); 59 | 60 | $audioFrame->destroy(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Rtmp/RtmpPublisherTrait.php: -------------------------------------------------------------------------------- 1 | publishStreamPath; 16 | } 17 | 18 | 19 | public function isAACSequence() 20 | { 21 | return $this->isAACSequence; 22 | } 23 | 24 | public function getAACSequenceFrame() 25 | { 26 | return $this->aacSequenceHeaderFrame; 27 | } 28 | 29 | public function isAVCSequence() 30 | { 31 | return $this->isAVCSequence; 32 | } 33 | 34 | public function getAVCSequenceFrame() 35 | { 36 | return $this->avcSequenceHeaderFrame; 37 | } 38 | 39 | 40 | public function isMetaData() 41 | { 42 | return $this->isMetaData; 43 | } 44 | 45 | public function getMetaDataFrame() 46 | { 47 | return $this->metaDataFrame; 48 | } 49 | 50 | public function hasAudio(){ 51 | return $this->isAACSequence(); 52 | } 53 | 54 | public function hasVideo(){ 55 | return $this->isAVCSequence(); 56 | } 57 | 58 | public function getGopCacheQueue(){ 59 | return $this->gopCacheQueue; 60 | } 61 | 62 | public function getPublishStreamInfo() 63 | { 64 | return [ 65 | "id"=>$this->id, 66 | "bytesRead"=>$this->bytesRead, 67 | "bytesReadRate"=>$this->bytesReadRate, 68 | "startTimestamp"=>$this->startTimestamp, 69 | "currentTimestamp"=>timestamp(), 70 | "publishStreamPath"=>$this->publishStreamPath, 71 | "videoWidth"=>$this->videoWidth, 72 | "videoHeight"=>$this->videoHeight, 73 | "videoFps"=> $this->videoFps, 74 | "videoCodecName"=>$this->videoCodecName, 75 | "videoProfileName"=>$this->videoProfileName, 76 | "videoLevel"=>$this->videoLevel, 77 | "audioSamplerate"=>$this->audioSamplerate, 78 | "audioChannels"=>$this->audioChannels, 79 | "audioCodecName"=>$this->audioCodecName, 80 | ]; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | info("[echo now] " . (timestamp() - $beginTime)); 37 | } 38 | } 39 | 40 | if (!function_exists('make_random_str')) { 41 | function make_random_str($length = 32) 42 | { 43 | static $char = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 44 | if (!is_int($length) || $length < 0) { 45 | return false; 46 | } 47 | $string = pack("@$length"); 48 | for ($i = 0, $clen = strlen($char); $i < $length; $i++) { 49 | $string[$i] = $char[mt_rand(0, $clen - 1)]; 50 | } 51 | return $string; 52 | } 53 | } 54 | 55 | 56 | if (!function_exists('generateNewSessionID')) { 57 | function generateNewSessionID($length = 8) 58 | { 59 | static $char = 'ABCDEFGHIJKLMNOPQRSTUVWKYZ0123456789'; 60 | if (!is_int($length) || $length < 0) { 61 | return false; 62 | } 63 | $string = pack("@$length"); 64 | for ($i = 0, $clen = strlen($char); $i < $length; $i++) { 65 | $string[$i] = $char[mt_rand(0, $clen - 1)]; 66 | } 67 | return $string; 68 | } 69 | } 70 | 71 | 72 | if (!function_exists('timestamp')) { 73 | function timestamp() 74 | { 75 | return floor(microtime(true) * 1000); 76 | } 77 | } 78 | 79 | if (!function_exists('is_assoc')) { 80 | function is_assoc($arr) 81 | { 82 | return array_keys($arr) !== range(0, count($arr) - 1); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/MediaReader/VideoFrame.php: -------------------------------------------------------------------------------- 1 | dump(); 53 | } 54 | 55 | 56 | public function getVideoCodecName() 57 | { 58 | return self::VIDEO_CODEC_NAME[$this->codecId]; 59 | } 60 | 61 | 62 | public function __construct($data, $timestamp = 0) 63 | { 64 | parent::__construct($data); 65 | 66 | $this->timestamp = $timestamp; 67 | $firstByte = $this->readTinyInt(); 68 | $this->frameType = $firstByte >> 4; 69 | $this->codecId = $firstByte & 15; 70 | } 71 | 72 | 73 | /** 74 | * @var AVCPacket 75 | */ 76 | protected $avcPacket; 77 | 78 | /** 79 | * @return AVCPacket 80 | */ 81 | public function getAVCPacket() 82 | { 83 | if (!$this->avcPacket) { 84 | $this->avcPacket = new AVCPacket($this); 85 | } 86 | 87 | return $this->avcPacket; 88 | } 89 | 90 | public function destroy(){ 91 | $this->avcPacket=null; 92 | } 93 | 94 | } -------------------------------------------------------------------------------- /src/Utils/WMBufferStream.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 24 | $this->connection->protocol = $this; 25 | $this->connection->onClose = [$this,'_onClose']; 26 | $this->connection->onError = [$this,'_onError']; 27 | 28 | parent::__construct(); 29 | 30 | } 31 | 32 | /* public function __destruct(){ 33 | logger()->info("WMBufferStream destruct"); 34 | }*/ 35 | 36 | public function _onClose($con){ 37 | $this->connection->protocol = null; 38 | $this->connection = null; 39 | $this->emit("onClose"); 40 | $this->removeAllListeners(); 41 | } 42 | public function _onError($con,$code,$msg){ 43 | $this->emit("onError"); 44 | } 45 | 46 | /** 47 | * @param $buffer string 48 | * @param $connection TcpConnection 49 | */ 50 | public static function input($buffer,$connection){ 51 | /** @var WMBufferStream $me */ 52 | $me = $connection->protocol; 53 | //reset recv buffer 54 | $me->recvBuffer($buffer); 55 | $me->emit("onData",[$me]); 56 | // clear connection recv buffer 57 | $me->clearConnectionRecvBuffer(); 58 | return 0; 59 | } 60 | 61 | public static function encode($buffer,$connection){ 62 | return $buffer; 63 | } 64 | 65 | public static function decode($buffer,$connection){ 66 | return $buffer; 67 | } 68 | 69 | 70 | 71 | public function recvBuffer($data){ 72 | $this->_data = $data; 73 | return $this->begin(); 74 | } 75 | 76 | public function recvSize(){ 77 | return strlen($this->_data); 78 | } 79 | 80 | public function handledSize(){ 81 | return $this->_index; 82 | } 83 | 84 | public function clearConnectionRecvBuffer(){ 85 | $this->connection->consumeRecvBuffer($this->_index); 86 | } 87 | 88 | 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/MediaReader/AACSequenceParameterSet.php: -------------------------------------------------------------------------------- 1 | readData(); 22 | } 23 | 24 | public function getAACProfileName() 25 | { 26 | switch ($this->objType) { 27 | case 1: 28 | return 'Main'; 29 | case 2: 30 | if ($this->ps > 0) { 31 | return 'HEv2'; 32 | } 33 | if ($this->sbr > 0) { 34 | return 'HE'; 35 | } 36 | return 'LC'; 37 | case 3: 38 | return 'SSR'; 39 | case 4: 40 | return 'LTP'; 41 | case 5: 42 | return 'SBR'; 43 | default: 44 | return ''; 45 | } 46 | } 47 | 48 | public function readData() 49 | { 50 | $objectType = ($objectType = $this->getBits(5)) === 31 ? ($this->getBits(6) + 32) : $objectType; 51 | $this->objType = $objectType; 52 | $sampleRate = ($sampleIndex = $this->getBits(4)) === 0x0f ? $this->getBits(24) : AACPacket::AAC_SAMPLE_RATE[$sampleIndex]; 53 | $this->sampleIndex = $sampleIndex; 54 | $this->sampleRate = $sampleRate; 55 | $channelConfig = $this->getBits(4); 56 | 57 | if ($channelConfig < count(AACPacket::AAC_CHANNELS)) { 58 | $channels = AACPacket::AAC_CHANNELS[$channelConfig]; 59 | $this->channels = $channels; 60 | } 61 | 62 | $this->sbr = -1; 63 | $this->ps = -1; 64 | if ($objectType == 5 || $objectType == 29) { 65 | if ($objectType == 29) { 66 | $this->ps = 1; 67 | } 68 | $this->extObjectType = 5; 69 | $this->sbr = 1; 70 | $this->sampleRate = ($sampleIndex = $this->getBits(4)) === 0x0f ? $this->getBits(24) : AACPacket::AAC_SAMPLE_RATE[$sampleIndex]; 71 | $this->sampleIndex = $sampleIndex; 72 | $this->objType = ($objectType = $this->getBits(5)) === 31 ? ($this->getBits(6) + 32) : $objectType; 73 | } 74 | 75 | 76 | } 77 | 78 | 79 | } -------------------------------------------------------------------------------- /packages/SabreAMF/OutputStream.php: -------------------------------------------------------------------------------- 1 | rawData.=$str; 31 | } 32 | 33 | /** 34 | * writeByte 35 | * 36 | * @param int $byte 37 | * @return void 38 | */ 39 | public function writeByte($byte) { 40 | 41 | $this->rawData.=pack('c',$byte); 42 | 43 | } 44 | 45 | /** 46 | * writeInt 47 | * 48 | * @param int $int 49 | * @return void 50 | */ 51 | public function writeInt($int) { 52 | 53 | $this->rawData.=pack('n',$int); 54 | 55 | } 56 | 57 | /** 58 | * writeDouble 59 | * 60 | * @param float $double 61 | * @return void 62 | */ 63 | public function writeDouble($double) { 64 | 65 | $bin = pack("d",$double); 66 | $testEndian = unpack("C*",pack("S*",256)); 67 | $bigEndian = !$testEndian[1]==1; 68 | if ($bigEndian) $bin = strrev($bin); 69 | $this->rawData.=$bin; 70 | 71 | } 72 | 73 | /** 74 | * writeLong 75 | * 76 | * @param int $long 77 | * @return void 78 | */ 79 | public function writeLong($long) { 80 | 81 | $this->rawData.=pack("N",$long); 82 | 83 | 84 | } 85 | 86 | /** 87 | * getRawData 88 | * 89 | * @return string 90 | */ 91 | public function getRawData() { 92 | 93 | return $this->rawData; 94 | 95 | } 96 | 97 | 98 | } 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/Rtmp/RtmpPacket.php: -------------------------------------------------------------------------------- 1 | chunkType = 0; 75 | $this->chunkStreamId = 0; 76 | $this->timestamp = 0; 77 | $this->length = 0; 78 | $this->type = 0; 79 | $this->streamId = 0; 80 | $this->hasAbsTimestamp = false; 81 | $this->hasExtTimestamp = false; 82 | $this->bytesRead = 0; 83 | $this->payload = ""; 84 | $this->state = self::PACKET_STATE_BEGIN; 85 | } 86 | 87 | public function free() 88 | { 89 | $this->payload = ""; 90 | $this->bytesRead = 0; 91 | } 92 | 93 | public function isReady() 94 | { 95 | return $this->bytesRead == $this->length; 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /src/Http/ExtHttpProtocol.php: -------------------------------------------------------------------------------- 1 | = 16384) { 25 | $connection->close("HTTP/1.1 413 Request Entity Too Large\r\n\r\n", true); 26 | return 0; 27 | } 28 | return 0; 29 | } 30 | 31 | $length = $crlf_pos + 4; 32 | $method = \strstr($recv_buffer, ' ', true); 33 | 34 | if (!\in_array($method, ['GET', 'POST', 'OPTIONS', 'HEAD', 'DELETE', 'PUT', 'PATCH'])) { 35 | $connection->close("HTTP/1.1 400 Bad Request\r\n\r\n", true); 36 | return 0; 37 | } 38 | 39 | $header = \substr($recv_buffer, 0, $crlf_pos); 40 | 41 | if(\preg_match("/\r\nUpgrade: websocket/i", $header)){ 42 | //upgrade websocket 43 | $connection->protocol = Websocket::class; 44 | return Websocket::input($recv_buffer,$connection); 45 | } 46 | 47 | if ($pos = \strpos($header, "\r\nContent-Length: ")) { 48 | $length = $length + (int)\substr($header, $pos + 18, 10); 49 | $has_content_length = true; 50 | } else if (\preg_match("/\r\ncontent-length: ?(\d+)/i", $header, $match)) { 51 | $length = $length + $match[1]; 52 | $has_content_length = true; 53 | } else { 54 | $has_content_length = false; 55 | if (false !== stripos($header, "\r\nTransfer-Encoding:")) { 56 | $connection->close("HTTP/1.1 400 Bad Request\r\n\r\n", true); 57 | return 0; 58 | } 59 | } 60 | 61 | if ($has_content_length) { 62 | if ($length > $connection->maxPackageSize) { 63 | $connection->close("HTTP/1.1 413 Request Entity Too Large\r\n\r\n", true); 64 | return 0; 65 | } 66 | } 67 | 68 | if (!isset($recv_buffer[512])) { 69 | //部分相同请求做缓存 相同请求做缓存 70 | $input[$recv_buffer] = $length; 71 | if (\count($input) > 512) { 72 | unset($input[key($input)]); 73 | } 74 | } 75 | 76 | return $length; 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /src/MediaReader/AudioFrame.php: -------------------------------------------------------------------------------- 1 | timestamp = $timestamp; 50 | $firstByte = $this->readTinyInt(); 51 | $this->soundFormat = $firstByte >> 4; 52 | $this->soundRate = $firstByte >> 2 & 3; 53 | $this->soundSize = $firstByte >> 1 & 1; 54 | $this->soundType = $firstByte & 1; 55 | 56 | } 57 | 58 | public function __toString() 59 | { 60 | return $this->dump(); 61 | } 62 | 63 | 64 | public function getAudioCodecName() 65 | { 66 | return self::AUDIO_CODEC_NAME[$this->soundFormat]; 67 | } 68 | 69 | public function getAudioSamplerate() 70 | { 71 | $rate = self::AUDIO_SOUND_RATE[$this->soundRate]; 72 | switch ($this->soundFormat) { 73 | case 4: 74 | $rate = 16000; 75 | break; 76 | case 5: 77 | $rate = 8000; 78 | break; 79 | case 11: 80 | $rate = 16000; 81 | break; 82 | case 14: 83 | $rate = 8000; 84 | break; 85 | } 86 | return $rate; 87 | } 88 | 89 | /** 90 | * @var AACPacket 91 | */ 92 | protected $aacPacket; 93 | 94 | /** 95 | * @return AACPacket 96 | */ 97 | public function getAACPacket() 98 | { 99 | if (!$this->aacPacket) { 100 | $this->aacPacket = new AACPacket($this); 101 | } 102 | 103 | return $this->aacPacket; 104 | } 105 | 106 | 107 | public function destroy() 108 | { 109 | $this->aacPacket = null; 110 | } 111 | 112 | } -------------------------------------------------------------------------------- /src/Rtmp/RtmpVideoHandlerTrait.php: -------------------------------------------------------------------------------- 1 | currentPacket; 21 | $videoFrame = new VideoFrame($p->payload, $p->clock); 22 | if ($this->videoCodec == 0) { 23 | $this->videoCodec = $videoFrame->codecId; 24 | $this->videoCodecName = $videoFrame->getVideoCodecName(); 25 | } 26 | 27 | 28 | if ($this->videoFps === 0) { 29 | //当前帧为第0 30 | if ($this->videoCount++ === 0) { 31 | $this->videoFpsCountTimer = Timer::add(5,function(){ 32 | $this->videoFps = ceil($this->videoCount / 5); 33 | $this->videoFpsCountTimer = null; 34 | },[],false); 35 | } 36 | } 37 | 38 | switch ($videoFrame->codecId) { 39 | case VideoFrame::VIDEO_CODEC_ID_AVC: 40 | //h264 41 | $avcPack = $videoFrame->getAVCPacket(); 42 | if ($avcPack->avcPacketType === AVCPacket::AVC_PACKET_TYPE_SEQUENCE_HEADER) { 43 | $this->isAVCSequence = true; 44 | $this->avcSequenceHeaderFrame = $videoFrame; 45 | $specificConfig = $avcPack->getAVCSequenceParameterSet(); 46 | $this->videoWidth = $specificConfig->width; 47 | $this->videoHeight = $specificConfig->height; 48 | $this->videoProfileName = $specificConfig->getAVCProfileName(); 49 | $this->videoLevel = $specificConfig->level; 50 | } 51 | if ($this->isAVCSequence) { 52 | if ($videoFrame->frameType === VideoFrame::VIDEO_FRAME_TYPE_KEY_FRAME 53 | && 54 | $avcPack->avcPacketType === AVCPacket::AVC_PACKET_TYPE_NALU) { 55 | $this->gopCacheQueue = []; 56 | } 57 | 58 | if ($videoFrame->frameType === VideoFrame::VIDEO_FRAME_TYPE_KEY_FRAME 59 | && 60 | $avcPack->avcPacketType === AVCPacket::AVC_PACKET_TYPE_SEQUENCE_HEADER) { 61 | //skip avc sequence 62 | } else { 63 | $this->gopCacheQueue[] = $videoFrame; 64 | } 65 | } 66 | 67 | break; 68 | } 69 | //数据处理与数据发送 70 | $this->emit('on_frame', [$videoFrame, $this]); 71 | //销毁AVC 72 | $videoFrame->destroy(); 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Rtmp/RtmpTrait.php: -------------------------------------------------------------------------------- 1 | info("[packet] {$p->type}"); 29 | //$b = memory_get_usage(); 30 | switch ($p->type) { 31 | case RtmpPacket::TYPE_SET_CHUNK_SIZE: 32 | case RtmpPacket::TYPE_ABORT: 33 | case RtmpPacket::TYPE_ACKNOWLEDGEMENT: 34 | case RtmpPacket::TYPE_WINDOW_ACKNOWLEDGEMENT_SIZE: 35 | case RtmpPacket::TYPE_SET_PEER_BANDWIDTH: 36 | //上面的类型全部进入协议控制信息处理流程 37 | 0 === $this->rtmpControlHandler() ? -1 : 0; 38 | break; 39 | case RtmpPacket::TYPE_EVENT: 40 | //event 信息进入event 处理流程,不处理 event 信息 41 | 0 === $this->rtmpEventHandler() ? -1 : 0; 42 | break; 43 | case RtmpPacket::TYPE_AUDIO: 44 | //audio 信息进入 audio 处理流程 45 | $this->rtmpAudioHandler(); 46 | break; 47 | case RtmpPacket::TYPE_VIDEO: 48 | //video 信息进入 video 处理流程 49 | $this->rtmpVideoHandler(); 50 | break; 51 | case RtmpPacket::TYPE_FLEX_MESSAGE: 52 | case RtmpPacket::TYPE_INVOKE: 53 | //上面信息进入invoke 引援?处理流程 54 | $this->rtmpInvokeHandler(); 55 | break; 56 | case RtmpPacket::TYPE_FLEX_STREAM: // AMF3 57 | case RtmpPacket::TYPE_DATA: // AMF0 58 | //其他rtmp信息处理 59 | $this->rtmpDataHandler(); 60 | break; 61 | } 62 | //logger()->info("[memory] memory add:" . (memory_get_usage() - $b)); 63 | } 64 | 65 | 66 | public function stop() 67 | { 68 | 69 | if ($this->isStarting) { 70 | $this->isStarting = false; 71 | if ($this->playStreamId > 0) { 72 | $this->onDeleteStream(['streamId' => $this->playStreamId]); 73 | } 74 | 75 | if ($this->publishStreamId > 0) { 76 | $this->onDeleteStream(['streamId' => $this->publishStreamId]); 77 | } 78 | 79 | if ($this->pingTimer) { 80 | Timer::del($this->pingTimer); 81 | $this->pingTimer = null; 82 | } 83 | 84 | if($this->videoFpsCountTimer){ 85 | Timer::del($this->videoFpsCountTimer); 86 | $this->videoFpsCountTimer = null; 87 | } 88 | 89 | if($this->dataCountTimer){ 90 | Timer::del($this->dataCountTimer); 91 | $this->dataCountTimer = null; 92 | } 93 | } 94 | 95 | $this->emit('on_close'); 96 | 97 | logger()->info("[rtmp disconnect] id={$this->id}"); 98 | 99 | 100 | } 101 | 102 | 103 | } 104 | -------------------------------------------------------------------------------- /packages/SabreAMF/InputStream.php: -------------------------------------------------------------------------------- 1 | rawData = $data; 46 | 47 | } 48 | 49 | /** 50 | * &readBuffer 51 | * 52 | * @param int $length 53 | * @return mixed 54 | */ 55 | public function &readBuffer($length) 56 | { 57 | 58 | if ($length + $this->cursor > strlen($this->rawData)) { 59 | throw new Exception('Buffer underrun at position: ' . $this->cursor . '. Trying to fetch ' . $length . ' bytes'); 60 | return false; 61 | } 62 | $data = substr($this->rawData, $this->cursor, $length); 63 | $this->cursor += $length; 64 | return $data; 65 | 66 | } 67 | 68 | /** 69 | * readByte 70 | * 71 | * @return int 72 | */ 73 | public function readByte() 74 | { 75 | 76 | return ord($this->readBuffer(1)); 77 | 78 | } 79 | 80 | /** 81 | * readInt 82 | * 83 | * @return int 84 | */ 85 | public function readInt() 86 | { 87 | 88 | $block = $this->readBuffer(2); 89 | $int = unpack("n", $block); 90 | return $int[1]; 91 | 92 | } 93 | 94 | 95 | /** 96 | * readDouble 97 | * 98 | * @return float 99 | */ 100 | public function readDouble() 101 | { 102 | 103 | $double = $this->readBuffer(8); 104 | 105 | $testEndian = unpack("C*", pack("S*", 256)); 106 | $bigEndian = !$testEndian[1] == 1; 107 | 108 | if ($bigEndian) $double = strrev($double); 109 | $double = unpack("d", $double); 110 | return $double[1]; 111 | } 112 | 113 | /** 114 | * readLong 115 | * 116 | * @return int 117 | */ 118 | public function readLong() 119 | { 120 | 121 | $block = $this->readBuffer(4); 122 | $long = unpack("N", $block); 123 | return $long[1]; 124 | } 125 | 126 | /** 127 | * readInt24 128 | * 129 | * return int 130 | */ 131 | public function readInt24() 132 | { 133 | 134 | $block = chr(0) . $this->readBuffer(3); 135 | $long = unpack("N", $block); 136 | return $long[1]; 137 | 138 | } 139 | 140 | public function isEnd() 141 | { 142 | return !isset($this->rawData[$this->cursor + 1]); 143 | } 144 | 145 | } 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /packages/SabreAMF/ArrayCollection.php: -------------------------------------------------------------------------------- 1 | data = new ArrayObject($data); 38 | 39 | } 40 | 41 | /** 42 | * This is used by SabreAMF when this object is unserialized (from AMF3) 43 | * 44 | * @param array $data 45 | * @return void 46 | */ 47 | function readExternal($data) { 48 | 49 | $this->data = new ArrayObject($data); 50 | 51 | } 52 | 53 | /** 54 | * This is used by SabreAMF when this object is serialized 55 | * 56 | * @return array 57 | */ 58 | function writeExternal() { 59 | 60 | return iterator_to_array($this->data); 61 | 62 | } 63 | 64 | /** 65 | * implemented from IteratorAggregate 66 | * 67 | * @return ArrayObject 68 | */ 69 | function getIterator() { 70 | 71 | return $this->data; 72 | 73 | } 74 | 75 | /** 76 | * implemented from ArrayAccess 77 | * 78 | * @param mixed $offset 79 | * @return bool 80 | */ 81 | function offsetExists($offset) { 82 | 83 | return isset($this->data[$offset]); 84 | 85 | } 86 | 87 | /** 88 | * Implemented from ArrayAccess 89 | * 90 | * @param mixed $offset 91 | * @return mixed 92 | */ 93 | function offsetGet($offset) { 94 | 95 | return $this->data[$offset]; 96 | 97 | } 98 | 99 | /** 100 | * Implemented from ArrayAccess 101 | * 102 | * @param mixed $offset 103 | * @param mixed $value 104 | * @return void 105 | */ 106 | function offsetSet($offset,$value) { 107 | 108 | if (!is_null($offset)) { 109 | $this->data[$offset] = $value; 110 | } else { 111 | $this->data[] = $value; 112 | } 113 | 114 | } 115 | 116 | /** 117 | * Implemented from ArrayAccess 118 | * 119 | * @param mixed $offset 120 | * @return void 121 | */ 122 | function offsetUnset($offset) { 123 | 124 | unset($this->data[$offset]); 125 | 126 | } 127 | 128 | /** 129 | * Implemented from Countable 130 | * 131 | * @return int 132 | */ 133 | function count() { 134 | 135 | return count($this->data); 136 | 137 | } 138 | 139 | } 140 | 141 | 142 | -------------------------------------------------------------------------------- /packages/SabreAMF/ClassMapper.php: -------------------------------------------------------------------------------- 1 | 'SabreAMF_AMF3_RemotingMessage', 26 | 'flex.messaging.messages.CommandMessage' => 'SabreAMF_AMF3_CommandMessage', 27 | 'flex.messaging.messages.AcknowledgeMessage' => 'SabreAMF_AMF3_AcknowledgeMessage', 28 | 'flex.messaging.messages.ErrorMessage' => 'SabreAMF_AMF3_ErrorMessage', 29 | 'flex.messaging.io.ArrayCollection' => 'SabreAMF_ArrayCollection' 30 | ); 31 | 32 | /** 33 | * Assign this callback to intercept calls to getLocalClass 34 | * 35 | * @var callback 36 | */ 37 | static public $onGetLocalClass; 38 | 39 | /** 40 | * Assign this callback to intercept calls to getRemoteClass 41 | * 42 | * @var callback 43 | */ 44 | static public $onGetRemoteClass; 45 | 46 | /** 47 | * The Constructor 48 | * 49 | * We make the constructor private so the class cannot be initialized 50 | * 51 | * @return void 52 | */ 53 | private function __construct() { } 54 | 55 | /** 56 | * Register a new class to be mapped 57 | * 58 | * @param string $remoteClass 59 | * @param string $localClass 60 | * @return void 61 | */ 62 | static public function registerClass($remoteClass,$localClass) { 63 | 64 | self::$maps[$remoteClass] = $localClass; 65 | 66 | } 67 | 68 | /** 69 | * Get the local classname for a remote class 70 | * 71 | * This method will return FALSE when the class is not found 72 | * 73 | * @param string $remoteClass 74 | * @return mixed 75 | */ 76 | static public function getLocalClass($remoteClass) { 77 | 78 | $localClass = false; 79 | $cb = false; 80 | $localClass=(isset(self::$maps[$remoteClass]))?self::$maps[$remoteClass]:false; 81 | if (!$localClass && is_callable(self::$onGetLocalClass)) { 82 | $cb = true; 83 | $localClass = call_user_func(self::$onGetLocalClass,$remoteClass); 84 | } 85 | if (!$localClass) return false; 86 | if (!is_string($localClass) && $cb) { 87 | throw new Exception('Classname received from onGetLocalClass should be a string or return false. ' . gettype($localClass) . ' was returned'); 88 | } 89 | if (!class_exists($localClass)) { 90 | throw new Exception('Class ' . $localClass . ' is not defined'); 91 | } 92 | return $localClass; 93 | 94 | } 95 | 96 | /** 97 | * Get the remote classname for a local class 98 | * 99 | * This method will return FALSE when the class is not found 100 | * 101 | * @param string $localClass 102 | * @return mixed 103 | */ 104 | static public function getRemoteClass($localClass) { 105 | 106 | $remoteClass = false; 107 | $cb = false; 108 | $remoteClass = array_search($localClass,self::$maps); 109 | if (!$remoteClass && is_callable(self::$onGetRemoteClass)) { 110 | $cb = true; 111 | $remoteClass = call_user_func(self::$onGetRemoteClass,$localClass); 112 | } 113 | if (!$remoteClass) return false; 114 | if (!is_string($remoteClass) && $cb) { 115 | throw new Exception('Classname received from onGetRemoteClass should be a string or return false. ' . gettype($remoteClass) . ' was returned'); 116 | } 117 | return $remoteClass; 118 | 119 | } 120 | 121 | } 122 | 123 | 124 | -------------------------------------------------------------------------------- /src/Utils/BinaryStream.php: -------------------------------------------------------------------------------- 1 | _data = $data; 13 | } 14 | 15 | public function reset() 16 | { 17 | $this->_index = 0; 18 | } 19 | 20 | public function skip($length) 21 | { 22 | $this->_index += $length; 23 | } 24 | 25 | public function flush($length = -1) 26 | { 27 | if ($length == -1) { 28 | $d = $this->_data; 29 | $this->_data = ""; 30 | } else { 31 | $d = substr($this->_data, 0, $length); 32 | $this->_data = substr($this->_data, $length); 33 | } 34 | $this->_index = 0; 35 | return $d; 36 | } 37 | 38 | 39 | public function dump() 40 | { 41 | return $this->_data; 42 | } 43 | 44 | public function has($len) 45 | { 46 | $pos = $len - 1; 47 | return isset($this->_data[$this->_index + $pos]); 48 | } 49 | 50 | public function clear() 51 | { 52 | $this->_data = substr($this->_data, $this->_index); 53 | $this->_index = 0; 54 | } 55 | 56 | public function begin() 57 | { 58 | $this->_index = 0; 59 | return $this; 60 | } 61 | 62 | public function move($pos) 63 | { 64 | $this->_index = max(array(0, min(array($pos, strlen($this->_data))))); 65 | return $this; 66 | } 67 | 68 | public function end() 69 | { 70 | $this->_index = strlen($this->_data); 71 | return $this; 72 | } 73 | 74 | public function push($data) 75 | { 76 | $this->_data .= $data; 77 | return $this; 78 | } 79 | //-------------------------------- 80 | // Writer 81 | //-------------------------------- 82 | 83 | public function writeByte($value) 84 | { 85 | $this->_data .= is_int($value) ? chr($value) : $value; 86 | $this->_index++; 87 | } 88 | 89 | public function writeInt16($value) 90 | { 91 | $this->_data .= pack("s", $value); 92 | $this->_index += 2; 93 | } 94 | 95 | public function writeInt24($value) 96 | { 97 | $this->_data .= substr(pack("N", $value), 1); 98 | $this->_index += 3; 99 | } 100 | 101 | public function writeInt32($value) 102 | { 103 | $this->_data .= pack("N", $value); 104 | $this->_index += 4; 105 | } 106 | 107 | public function writeInt32LE($value) 108 | { 109 | $this->_data .= pack("V", $value); 110 | $this->_index += 4; 111 | } 112 | 113 | public function write($value) 114 | { 115 | $this->_data .= $value; 116 | $this->_index += strlen($value); 117 | } 118 | 119 | //------------------------------- 120 | // Reader 121 | //------------------------------- 122 | 123 | public function readByte() 124 | { 125 | return ($this->_data[$this->_index++]); 126 | } 127 | 128 | public function readTinyInt() 129 | { 130 | return ord($this->readByte()); 131 | } 132 | 133 | public function readInt16() 134 | { 135 | return ($this->readTinyInt() << 8) + $this->readTinyInt(); 136 | } 137 | 138 | public function readInt16LE() 139 | { 140 | return $this->readTinyInt() + ($this->readTinyInt() << 8); 141 | } 142 | 143 | public function readInt24() 144 | { 145 | $m = unpack("N", "\x00" . substr($this->_data, $this->_index, 3)); 146 | $this->_index += 3; 147 | return $m[1]; 148 | } 149 | 150 | public function readInt32() 151 | { 152 | return $this->read("N", 4); 153 | } 154 | 155 | public function readInt32LE() 156 | { 157 | return $this->read("V", 4); 158 | } 159 | 160 | public function readRaw($length = 0) 161 | { 162 | if ($length == 0) 163 | $length = strlen($this->_data) - $this->_index; 164 | $datas = substr($this->_data, $this->_index, $length); 165 | $this->_index += $length; 166 | return $datas; 167 | } 168 | 169 | private function read($type, $size) 170 | { 171 | $m = unpack("$type", substr($this->_data, $this->_index, $size)); 172 | $this->_index += $size; 173 | return $m[1]; 174 | } 175 | 176 | //------------------------------- 177 | // Tag & rollback 178 | //------------------------------- 179 | 180 | protected $tagPos; 181 | 182 | public function tag() 183 | { 184 | $this->tagPos = $this->_index; 185 | } 186 | 187 | public function rollBack() 188 | { 189 | $this->_index = $this->tagPos; 190 | } 191 | 192 | 193 | } 194 | -------------------------------------------------------------------------------- /src/Rtmp/RtmpPacketTrait.php: -------------------------------------------------------------------------------- 1 | buffer; 23 | 24 | 25 | /** @var RtmpPacket $p */ 26 | $p = $this->currentPacket; 27 | switch ($p->state) { 28 | case RtmpPacket::PACKET_STATE_MSG_HEADER: 29 | //base header + message header 30 | if ($stream->has($p->msgHeaderLen)) { 31 | switch ($p->chunkType) { 32 | case RtmpChunk::CHUNK_TYPE_3: 33 | // all same 34 | break; 35 | case RtmpChunk::CHUNK_TYPE_2: 36 | //new timestamp delta, 3bytes 37 | $p->timestamp = $stream->readInt24(); 38 | break; 39 | case RtmpChunk::CHUNK_TYPE_1: 40 | //new timestamp delta, length,type 7bytes 41 | $p->timestamp = $stream->readInt24(); 42 | $p->length = $stream->readInt24(); 43 | $p->type = $stream->readTinyInt(); 44 | break; 45 | case RtmpChunk::CHUNK_TYPE_0: 46 | //all different, 11bytes 47 | $p->timestamp = $stream->readInt24(); 48 | $p->length = $stream->readInt24(); 49 | $p->type = $stream->readTinyInt(); 50 | $p->streamId = $stream->readInt32LE(); 51 | break; 52 | } 53 | 54 | if ($p->chunkType == RtmpChunk::CHUNK_TYPE_0) { 55 | //当前时间是绝对时间 56 | $p->hasAbsTimestamp = true; 57 | } 58 | 59 | 60 | $p->state = RtmpPacket::PACKET_STATE_EXT_TIMESTAMP; 61 | 62 | //logger()->info("chunk header fin"); 63 | } else { 64 | //长度不够,等待下个数据包 65 | return false; 66 | } 67 | case RtmpPacket::PACKET_STATE_EXT_TIMESTAMP: 68 | if ($p->timestamp === RtmpPacket::MAX_TIMESTAMP) { 69 | if ($stream->has(4)) { 70 | $extTimestamp = $stream->readInt32(); 71 | logger()->info("chunk has ext timestamp {$extTimestamp}"); 72 | $p->hasExtTimestamp = true; 73 | } else { 74 | //当前长度不够,等待下个数据包 75 | return false; 76 | } 77 | } else { 78 | $extTimestamp = $p->timestamp; 79 | } 80 | 81 | //判断当前包是不是有数据 82 | if ($p->bytesRead == 0) { 83 | if ($p->chunkType == RtmpChunk::CHUNK_TYPE_0) { 84 | $p->clock = $extTimestamp; 85 | } else { 86 | $p->clock += $extTimestamp; 87 | } 88 | 89 | } 90 | 91 | $p->state = RtmpPacket::PACKET_STATE_PAYLOAD; 92 | case RtmpPacket::PACKET_STATE_PAYLOAD: 93 | 94 | $size = min( 95 | $this->inChunkSize, //读取完整的包 96 | $p->length - $p->bytesRead //当前剩余的数据 97 | ); 98 | 99 | 100 | if ($size > 0) { 101 | if ($stream->has($size)) { 102 | //数据拷贝 103 | $p->payload .= $stream->readRaw($size); 104 | $p->bytesRead += $size; 105 | //logger()->info("packet csid {$p->chunkStreamId} stream {$p->streamId} payload size {$size} payload size: {$p->length} bytesRead {$p->bytesRead}"); 106 | } else { 107 | //长度不够,等待下个数据包 108 | //logger()->info("packet csid {$p->chunkStreamId} stream {$p->streamId} payload size {$size} payload size: {$p->length} bytesRead {$p->bytesRead} buffer ") . " not enough."); 109 | return false; 110 | } 111 | } 112 | 113 | if ($p->isReady()) { 114 | //开始读取下一个包 115 | $this->chunkState = RtmpChunk::CHUNK_STATE_BEGIN; 116 | 117 | $this->rtmpHandler($p); 118 | 119 | //当前包已经读取完成数据,释放当前包 120 | $p->free(); 121 | } elseif (0 === $p->bytesRead % $this->inChunkSize) { 122 | //当前chunk已经读取完成 123 | $this->chunkState = RtmpChunk::CHUNK_STATE_BEGIN; 124 | } 125 | } 126 | 127 | 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Rtmp/RtmpPlayerTrait.php: -------------------------------------------------------------------------------- 1 | isPlayerIdling; 23 | } 24 | 25 | public function isEnableAudio() 26 | { 27 | return true; 28 | } 29 | 30 | public function isEnableVideo() 31 | { 32 | return true; 33 | } 34 | 35 | public function isEnableGop() 36 | { 37 | return true; 38 | } 39 | 40 | public function setEnableAudio($status) 41 | { 42 | } 43 | 44 | public function setEnableVideo($status) 45 | { 46 | } 47 | 48 | public function setEnableGop($status) 49 | { 50 | } 51 | 52 | 53 | 54 | /** 55 | * 播放开始 56 | * @return mixed 57 | */ 58 | public function startPlay() 59 | { 60 | 61 | //各种发送数据包 62 | $path = $this->getPlayPath(); 63 | $publishStream = MediaServer::getPublishStream($path); 64 | /** 65 | * meta data send 66 | */ 67 | if ($publishStream->isMetaData()) { 68 | $metaDataFrame = $publishStream->getMetaDataFrame(); 69 | $this->sendMetaDataFrame($metaDataFrame); 70 | } 71 | 72 | /** 73 | * avc sequence send 74 | */ 75 | if ($publishStream->isAVCSequence()) { 76 | $avcFrame = $publishStream->getAVCSequenceFrame(); 77 | $this->sendVideoFrame($avcFrame); 78 | } 79 | 80 | 81 | /** 82 | * aac sequence send 83 | */ 84 | if ($publishStream->isAACSequence()) { 85 | $aacFrame = $publishStream->getAACSequenceFrame(); 86 | $this->sendAudioFrame($aacFrame); 87 | } 88 | 89 | //gop 发送 90 | if ($this->enableGop) { 91 | foreach ($publishStream->getGopCacheQueue() as &$frame) { 92 | $this->frameSend($frame); 93 | } 94 | } 95 | 96 | $this->isPlayerIdling = false; 97 | $this->isPlaying = true; 98 | } 99 | 100 | /** 101 | * @param $frame MediaFrame 102 | * @return mixed 103 | */ 104 | public function frameSend($frame) 105 | { 106 | switch ($frame->FRAME_TYPE) { 107 | case MediaFrame::VIDEO_FRAME: 108 | return $this->sendVideoFrame($frame); 109 | case MediaFrame::AUDIO_FRAME: 110 | return $this->sendAudioFrame($frame); 111 | case MediaFrame::META_FRAME: 112 | return $this->sendMetaDataFrame($frame); 113 | } 114 | } 115 | 116 | /** 117 | * @param $metaDataFrame MetaDataFrame|MediaFrame 118 | * @return mixed 119 | */ 120 | public function sendMetaDataFrame($metaDataFrame) 121 | { 122 | $packet = new RtmpPacket(); 123 | $packet->chunkType = RtmpChunk::CHUNK_TYPE_0; 124 | $packet->chunkStreamId = RtmpChunk::CHANNEL_DATA; 125 | $packet->type = RtmpPacket::TYPE_DATA; 126 | $packet->payload = (string)$metaDataFrame; 127 | $packet->length = strlen($packet->payload); 128 | $packet->streamId = $this->playStreamId; 129 | $chunks = $this->rtmpChunksCreate($packet); 130 | $this->write($chunks); 131 | } 132 | 133 | /** 134 | * @param $audioFrame AudioFrame|MediaFrame 135 | * @return mixed 136 | */ 137 | public function sendAudioFrame($audioFrame) 138 | { 139 | $packet = new RtmpPacket(); 140 | $packet->chunkType = RtmpChunk::CHUNK_TYPE_0; 141 | $packet->chunkStreamId = RtmpChunk::CHANNEL_AUDIO; 142 | $packet->type = RtmpPacket::TYPE_AUDIO; 143 | $packet->payload = (string)$audioFrame; 144 | $packet->timestamp = $audioFrame->timestamp; 145 | $packet->length = strlen($packet->payload); 146 | $packet->streamId = $this->playStreamId; 147 | $chunks = $this->rtmpChunksCreate($packet); 148 | $this->write($chunks); 149 | } 150 | 151 | /** 152 | * @param $videoFrame VideoFrame|MediaFrame 153 | * @return mixed 154 | */ 155 | public function sendVideoFrame($videoFrame) 156 | { 157 | $packet = new RtmpPacket(); 158 | $packet->chunkType = RtmpChunk::CHUNK_TYPE_0; 159 | $packet->chunkStreamId = RtmpChunk::CHANNEL_VIDEO; 160 | $packet->type = RtmpPacket::TYPE_VIDEO; 161 | $packet->payload = (string)$videoFrame; 162 | $packet->length = strlen($packet->payload); 163 | $packet->streamId = $this->playStreamId; 164 | $packet->timestamp = $videoFrame->timestamp; 165 | $chunks = $this->rtmpChunksCreate($packet); 166 | $this->write($chunks); 167 | } 168 | 169 | /** 170 | * @return mixed 171 | */ 172 | public function playClose() 173 | { 174 | $this->stop(); 175 | $this->input->close(); 176 | } 177 | 178 | /** 179 | * 获取当前路径 180 | * @return string 181 | */ 182 | public function getPlayPath() 183 | { 184 | return $this->playStreamPath; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Flv/Flv.php: -------------------------------------------------------------------------------- 1 | ord($tagData[0]), 36 | 'dataSize' => strlen($tagData) - 11, 37 | 'timestamp' => (ord($tagData[7]) << 24) | (ord($tagData[4]) << 16) | (ord($tagData[5]) << 8) | ord($tagData[6]), 38 | 'streamId' => (ord($tagData[8]) << 16) | (ord($tagData[9]) << 8) | ord($tagData[10]), 39 | //'data'=>substr($tagData,11), 40 | 'data' => substr($tagData, 11) 41 | ]; 42 | } 43 | 44 | /** 45 | * @param $scriptData 46 | * @return null[] 47 | * @throws Exception 48 | */ 49 | static function scriptFrameDataRead($scriptData) 50 | { 51 | static $scriptMetaDataCode = [ 52 | 'onMetaData' => ['dataObj'] 53 | ]; 54 | $stream = new SabreAMF_InputStream($scriptData); 55 | $deserializer = new SabreAMF_AMF0_Deserializer($stream); 56 | $result = [ 57 | 'cmd' => null, 58 | ]; 59 | if ($cmd = @$deserializer->readAMFData()) { 60 | $result['cmd'] = $cmd; 61 | if (isset($scriptMetaDataCode[$cmd])) { 62 | foreach ($scriptMetaDataCode[$cmd] as $k) { 63 | $result[$k] = $deserializer->readAMFData(); 64 | } 65 | } else { 66 | logger()->warning('AMF Unknown command {cmd}', $result); 67 | } 68 | } else { 69 | logger()->warning('AMF read data error'); 70 | } 71 | return $result; 72 | } 73 | 74 | static function videoFrameDataRead($videoData) 75 | { 76 | $firstByte = ord($videoData[0]); 77 | return [ 78 | 'frameType' => $firstByte >> 4, 79 | 'codecId' => $firstByte & 15, 80 | 'data' => substr($videoData, 1), 81 | ]; 82 | } 83 | 84 | static function avcPacketRead($avcPacket) 85 | { 86 | return [ 87 | 'avcPacketType' => ord($avcPacket[0]), //if codecId == 7 ,0 avc sequence header,1 avc nalus 88 | 'compositionTime' => (ord($avcPacket[1]) << 16) | (ord($avcPacket[2]) << 8) | ord($avcPacket[3]), 89 | 'data' => substr($avcPacket, 4) 90 | ]; 91 | } 92 | 93 | static function audioFrameDataRead($audioData) 94 | { 95 | $firstByte = ord($audioData[0]); 96 | return [ 97 | 'soundFormat' => $firstByte >> 4, 98 | 'soundRate' => $firstByte >> 2 & 3, 99 | 'soundSize' => $firstByte >> 1 & 1, 100 | 'soundType' => $firstByte & 1, 101 | 'data' => substr($audioData, 1) 102 | ]; 103 | } 104 | 105 | static function accPacketDataRead($accData) 106 | { 107 | return [ 108 | 'accPacketType' => ord($accData[0]), //0 = AAC sequence header,1 = AAC raw 109 | 'data' => substr($accData, 1) 110 | ]; 111 | } 112 | 113 | 114 | /** 115 | * $analysis = unpack("CtagType/a3tagSize/a3timestamp/CtimestampEx/a3streamId/a{$dataSize}data", $data); 116 | * $tag = [ 117 | * 'type' => $analysis['tagType'], 118 | * 'dataSize' => $dataSize, 119 | * 'timestamp' => ($analysis['timestampEx'] << 24) | (\ord($analysis['timestamp'][0]) << 16) | (\ord($analysis['timestamp'][1]) << 8) | \ord($analysis['timestamp'][2]), 120 | * 'streamId' => (\ord($analysis['streamId'][0]) << 16) | (\ord($analysis['streamId'][1]) << 8) | \ord($analysis['streamId'][2]), 121 | * 'data' => $analysis['data'] 122 | * ]; 123 | * 124 | * @param $tag FlvTag 125 | * @return string 126 | */ 127 | static function createFlvTag($tag) 128 | { 129 | $preTagLen = 11 +$tag->dataSize; 130 | $packet = pack("Ca3a3Ca3a{$tag->dataSize}N", 131 | $tag->type, //type 132 | pack("N", $tag->dataSize << 8), //dataSize 133 | pack("N", $tag->timestamp << 8), //timeStamp 134 | $tag->timestamp >> 24, //timeStampExt 135 | pack("N", $tag->streamId<< 8), //streamId 136 | $tag->data, //data 137 | $preTagLen //preTagLen 138 | ); 139 | 140 | return $packet; 141 | } 142 | 143 | const SCRIPT_TAG = 18; 144 | const AUDIO_TAG = 8; 145 | const VIDEO_TAG = 9; 146 | 147 | const VIDEO_FRAME_TYPE_KEY_FRAME = 1; 148 | const VIDEO_FRAME_TYPE_INTER_FRAME = 2; 149 | const VIDEO_FRAME_TYPE_DISPOSABLE_INTER_FRAME = 3; 150 | const VIDEO_FRAME_TYPE_GENERATED_KEY_FRAME = 4; 151 | const VIDEO_FRAME_TYPE_VIDEO_INFO_FRAME = 5; 152 | 153 | const VIDEO_CODEC_ID_JPEG = 1; 154 | const VIDEO_CODEC_ID_H263 = 2; 155 | const VIDEO_CODEC_ID_SCREEN = 3; 156 | const VIDEO_CODEC_ID_VP6_FLV = 4; 157 | const VIDEO_CODEC_ID_VP6_FLV_ALPHA = 5; 158 | const VIDEO_CODEC_ID_SCREEN_V2 = 6; 159 | const VIDEO_CODEC_ID_AVC = 7; 160 | 161 | const AVC_PACKET_TYPE_SEQUENCE_HEADER = 0; 162 | const AVC_PACKET_TYPE_NALU = 1; 163 | const AVC_PACKET_TYPE_END_SEQUENCE = 2; 164 | 165 | 166 | const SOUND_FORMAT_ACC = 10; 167 | 168 | const ACC_PACKET_TYPE_SEQUENCE_HEADER = 0; 169 | const ACC_PACKET_TYPE_RAW = 1; 170 | } 171 | -------------------------------------------------------------------------------- /src/Rtmp/RtmpAMF.php: -------------------------------------------------------------------------------- 1 | ['transId', 'cmdObj', 'info'], 17 | '_error' => ['transId', 'cmdObj', 'info', 'streamId'], // Info / Streamid are optional 18 | 'onStatus' => ['transId', 'cmdObj', 'info'], 19 | 'releaseStream' => ['transId', 'cmdObj', 'streamName'], 20 | 'getStreamLength' => ['transId', 'cmdObj', 'streamId'], 21 | 'getMovLen' => ['transId', 'cmdObj', 'streamId'], 22 | 'FCPublish' => ['transId', 'cmdObj', 'streamName'], 23 | 'FCUnpublish' => ['transId', 'cmdObj', 'streamName'], 24 | 'FCSubscribe' => ['transId', 'cmdObj', 'streamName'], 25 | 'onFCPublish' => ['transId', 'cmdObj', 'info'], 26 | 'connect' => ['transId', 'cmdObj', 'args'], 27 | 'call' => ['transId', 'cmdObj', 'args'], 28 | 'createStream' => ['transId', 'cmdObj'], 29 | 'close' => ['transId', 'cmdObj'], 30 | 'play' => ['transId', 'cmdObj', 'streamName', 'start', 'duration', 'reset'], 31 | 'play2' => ['transId', 'cmdObj', 'params'], 32 | 'deleteStream' => ['transId', 'cmdObj', 'streamId'], 33 | 'closeStream' => ['transId', 'cmdObj'], 34 | 'receiveAudio' => ['transId', 'cmdObj', 'bool'], 35 | 'receiveVideo' => ['transId', 'cmdObj', 'bool'], 36 | 'publish' => ['transId', 'cmdObj', 'streamName', 'type'], 37 | 'seek' => ['transId', 'cmdObj', 'ms'], 38 | 'pause' => ['transId', 'cmdObj', 'pause', 'ms'] 39 | ]; 40 | 41 | const RTMP_DATA_CODE = [ 42 | '@setDataFrame' => ['method', 'dataObj'], 43 | 'onFI' => ['info'], 44 | 'onMetaData' => ['dataObj'], 45 | '|RtmpSampleAccess' => ['bool1', 'bool2'], 46 | ]; 47 | 48 | 49 | /** 50 | * @param $payload 51 | * @return null[] 52 | * @throws \Exception 53 | */ 54 | static function rtmpCMDAmf0Reader($payload) 55 | { 56 | $stream = new \SabreAMF_InputStream($payload); 57 | $deserializer = new \SabreAMF_AMF0_Deserializer($stream); 58 | $result = [ 59 | 'cmd' => null, 60 | ]; 61 | if ($cmd = @$deserializer->readAMFData()) { 62 | $result['cmd'] = $cmd; 63 | if (isset(self::RTMP_CMD_CODE[$cmd])) { 64 | foreach (self::RTMP_CMD_CODE[$cmd] as $k) { 65 | if ($stream->isEnd()) { 66 | break; 67 | } 68 | $result[$k] = $deserializer->readAMFData(); 69 | } 70 | } else { 71 | logger()->warning('AMF Unknown command {cmd}', $result); 72 | } 73 | } else { 74 | logger()->warning('AMF read data error'); 75 | } 76 | 77 | return $result; 78 | } 79 | 80 | 81 | /** 82 | * @param $payload 83 | * @return null[] 84 | * @throws \Exception 85 | */ 86 | static function rtmpDataAmf0Reader($payload) 87 | { 88 | 89 | $stream = new \SabreAMF_InputStream($payload); 90 | $deserializer = new \SabreAMF_AMF0_Deserializer($stream); 91 | $result = [ 92 | 'cmd' => null, 93 | ]; 94 | if ($cmd = @$deserializer->readAMFData()) { 95 | $result['cmd'] = $cmd; 96 | if (isset(self::RTMP_DATA_CODE[$cmd])) { 97 | foreach (self::RTMP_DATA_CODE[$cmd] as $k) { 98 | if ($stream->isEnd()) { 99 | break; 100 | } 101 | $result[$k] = $deserializer->readAMFData(); 102 | } 103 | } else { 104 | logger()->warning('AMF Unknown command {cmd}', $result); 105 | } 106 | } else { 107 | logger()->warning('AMF read data error'); 108 | } 109 | return $result; 110 | } 111 | 112 | /** 113 | * Encode AMF0 Command 114 | * @param $opt 115 | * @throws \Exception 116 | * @return string 117 | */ 118 | static function rtmpCMDAmf0Creator($opt) 119 | { 120 | 121 | $outputStream = new \SabreAMF_OutputStream(); 122 | $serializer = new \SabreAMF_AMF0_Serializer($outputStream); 123 | $serializer->writeAMFData($opt['cmd']); 124 | if (isset(self::RTMP_CMD_CODE[$opt['cmd']])) { 125 | foreach (self::RTMP_CMD_CODE[$opt['cmd']] as $k) { 126 | if (key_exists($k, $opt)) { 127 | $serializer->writeAMFData($opt[$k]); 128 | } else { 129 | logger()->debug("amf 0 create {$k} not in opt " . json_encode($opt)); 130 | } 131 | } 132 | } else { 133 | logger()->debug('AMF Unknown command {cmd}', $opt); 134 | } 135 | //logger()->debug('Encoded as ' . bin2hex($outputStream->getRawData())); 136 | return $outputStream->getRawData(); 137 | } 138 | 139 | /** 140 | * Encode AMF0 Command 141 | * @param $opt 142 | * @throws \Exception 143 | * @return string 144 | */ 145 | static function rtmpDATAAmf0Creator($opt) 146 | { 147 | 148 | $outputStream = new \SabreAMF_OutputStream(); 149 | $serializer = new \SabreAMF_AMF0_Serializer($outputStream); 150 | $serializer->writeAMFData($opt['cmd']); 151 | if (isset(self::RTMP_DATA_CODE[$opt['cmd']])) { 152 | foreach (self::RTMP_DATA_CODE[$opt['cmd']] as $k) { 153 | if (key_exists($k, $opt)) { 154 | $serializer->writeAMFData($opt[$k]); 155 | } 156 | } 157 | } else { 158 | logger()->debug('AMF Unknown command {cmd}', $opt); 159 | } 160 | //logger()->debug('Encoded as' . bin2hex($outputStream->getRawData())); 161 | return $outputStream->getRawData(); 162 | } 163 | } -------------------------------------------------------------------------------- /src/Flv/FlvPlayStream.php: -------------------------------------------------------------------------------- 1 | input = $input; 43 | $input->on('error', [$this, 'onStreamError']); 44 | $input->on('close', [$this, 'close']); 45 | $this->playPath = $playPath; 46 | } 47 | 48 | public function __destruct() 49 | { 50 | logger()->info("player flv stream {path} destruct", ['path' => $this->playPath]); 51 | } 52 | 53 | /** 54 | * @param \Exception $e 55 | * @internal 56 | */ 57 | public function onStreamError(\Exception $e) 58 | { 59 | $this->close(); 60 | } 61 | 62 | public function close() 63 | { 64 | if ($this->closed) { 65 | return; 66 | } 67 | 68 | $this->closed = true; 69 | $this->input->close(); 70 | $this->emit('on_close'); 71 | $this->removeAllListeners(); 72 | } 73 | 74 | 75 | public function isPlayerIdling() 76 | { 77 | return $this->isPlayerIdling; 78 | } 79 | 80 | public function write($data) 81 | { 82 | return $this->input->write($data); 83 | } 84 | 85 | public function isEnableAudio() 86 | { 87 | return true; 88 | } 89 | 90 | public function isEnableVideo() 91 | { 92 | return true; 93 | } 94 | 95 | public function isEnableGop() 96 | { 97 | return true; 98 | } 99 | 100 | public function setEnableAudio($status) 101 | { 102 | } 103 | 104 | public function setEnableVideo($status) 105 | { 106 | } 107 | 108 | public function setEnableGop($status) 109 | { 110 | } 111 | 112 | 113 | public function startPlay() 114 | { 115 | //各种发送数据包 116 | $path = $this->getPlayPath(); 117 | $publishStream = MediaServer::getPublishStream($path); 118 | logger()->info('flv play stream start play'); 119 | 120 | if (!$this->isFlvHeader) { 121 | $flvHeader = "FLV\x01\x00" . pack('NN', 9, 0); 122 | if ($this->isEnableAudio() && $publishStream->hasAudio()) { 123 | $flvHeader[4] = chr(ord($flvHeader[4]) | 4); 124 | } 125 | if ($this->isEnableVideo() && $publishStream->hasVideo()) { 126 | $flvHeader[4] = chr(ord($flvHeader[4]) | 1); 127 | } 128 | $this->write($flvHeader); 129 | $this->isFlvHeader = true; 130 | } 131 | 132 | 133 | /** 134 | * meta data send 135 | */ 136 | if ($publishStream->isMetaData()) { 137 | $metaDataFrame = $publishStream->getMetaDataFrame(); 138 | $this->sendMetaDataFrame($metaDataFrame); 139 | } 140 | 141 | /** 142 | * avc sequence send 143 | */ 144 | if ($publishStream->isAVCSequence()) { 145 | $avcFrame = $publishStream->getAVCSequenceFrame(); 146 | $this->sendVideoFrame($avcFrame); 147 | } 148 | 149 | 150 | /** 151 | * aac sequence send 152 | */ 153 | if ($publishStream->isAACSequence()) { 154 | $aacFrame = $publishStream->getAACSequenceFrame(); 155 | $this->sendAudioFrame($aacFrame); 156 | } 157 | 158 | //gop 发送 159 | if ($this->isEnableGop()) { 160 | foreach ($publishStream->getGopCacheQueue() as &$frame) { 161 | $this->frameSend($frame); 162 | } 163 | } 164 | 165 | $this->isPlayerIdling = false; 166 | $this->isPlaying = true; 167 | } 168 | 169 | /** 170 | * @param $frame MediaFrame 171 | * @return mixed 172 | */ 173 | public function frameSend($frame) 174 | { 175 | // logger()->info("send ".get_class($frame)." timestamp:".($frame->timestamp??0)); 176 | switch ($frame->FRAME_TYPE) { 177 | case MediaFrame::VIDEO_FRAME: 178 | return $this->sendVideoFrame($frame); 179 | case MediaFrame::AUDIO_FRAME: 180 | return $this->sendAudioFrame($frame); 181 | case MediaFrame::META_FRAME: 182 | return $this->sendMetaDataFrame($frame); 183 | } 184 | } 185 | 186 | public function playClose() 187 | { 188 | $this->input->close(); 189 | } 190 | 191 | public function getPlayPath() 192 | { 193 | return $this->playPath; 194 | } 195 | 196 | 197 | /** 198 | * @param $metaDataFrame MetaDataFrame|MediaFrame 199 | * @return mixed 200 | */ 201 | public function sendMetaDataFrame($metaDataFrame) 202 | { 203 | $tag = new FlvTag(); 204 | $tag->type = Flv::SCRIPT_TAG; 205 | $tag->timestamp = 0; 206 | $tag->data = (string)$metaDataFrame; 207 | $tag->dataSize = strlen($tag->data); 208 | $chunks = Flv::createFlvTag($tag); 209 | $this->write($chunks); 210 | } 211 | 212 | /** 213 | * @param $audioFrame AudioFrame|MediaFrame 214 | * @return mixed 215 | */ 216 | public function sendAudioFrame($audioFrame) 217 | { 218 | $tag = new FlvTag(); 219 | $tag->type = Flv::AUDIO_TAG; 220 | $tag->timestamp = $audioFrame->timestamp; 221 | $tag->data = (string)$audioFrame; 222 | $tag->dataSize = strlen($tag->data); 223 | $chunks = Flv::createFlvTag($tag); 224 | $this->write($chunks); 225 | } 226 | 227 | /** 228 | * @param $videoFrame VideoFrame|MediaFrame 229 | * @return mixed 230 | */ 231 | public function sendVideoFrame($videoFrame) 232 | { 233 | $tag = new FlvTag(); 234 | $tag->type = Flv::VIDEO_TAG; 235 | $tag->timestamp = $videoFrame->timestamp; 236 | $tag->data = (string)$videoFrame; 237 | $tag->dataSize = strlen($tag->data); 238 | $chunks = Flv::createFlvTag($tag); 239 | $this->write($chunks); 240 | } 241 | 242 | 243 | } 244 | -------------------------------------------------------------------------------- /src/MediaServer.php: -------------------------------------------------------------------------------- 1 | getPublishStreamInfo() 49 | ]:[]; 50 | } 51 | return array_map(function($stream){ 52 | return $stream->getPublishStreamInfo(); 53 | },array_values(self::$publishStream)); 54 | } 55 | 56 | /** 57 | * @param $path 58 | * @return bool 59 | */ 60 | static public function hasPublishStream($path) 61 | { 62 | return isset(self::$publishStream[$path]); 63 | } 64 | 65 | /** 66 | * @param $path 67 | * @return PublishStreamInterface 68 | */ 69 | static public function getPublishStream($path) 70 | { 71 | return self::$publishStream[$path]; 72 | } 73 | 74 | /** 75 | * @param $stream PublishStreamInterface 76 | */ 77 | static protected function addPublishStream($stream) 78 | { 79 | $path = $stream->getPublishPath(); 80 | self::$publishStream[$path] = $stream; 81 | } 82 | 83 | static protected function delPublishStream($path) 84 | { 85 | unset(self::$publishStream[$path]); 86 | } 87 | 88 | /** 89 | * @var PlayStreamInterface[][] 90 | */ 91 | static public $playerStream = []; 92 | 93 | /** 94 | * @param $path 95 | * @return array|PlayStreamInterface[] 96 | */ 97 | static public function getPlayStreams($path) 98 | { 99 | return self::$playerStream[$path] ?? []; 100 | } 101 | 102 | 103 | /** 104 | * @param $path 105 | * @param $objId 106 | */ 107 | static protected function delPlayerStream($path, $objId) 108 | { 109 | unset(self::$playerStream[$path][$objId]); 110 | //一个播放设备都没有 111 | if (self::hasPublishStream($path) && count(self::getPlayStreams($path)) == 0) { 112 | $p_stream = self::getPublishStream($path); 113 | $p_stream->removeListener('on_frame', self::class . '::publisherOnFrame'); 114 | $p_stream->is_on_frame = false; 115 | } 116 | } 117 | 118 | /** 119 | * @param $playerStream PlayStreamInterface 120 | */ 121 | static protected function addPlayerStream($playerStream) 122 | { 123 | 124 | $path = $playerStream->getPlayPath(); 125 | $objIndex = spl_object_id($playerStream); 126 | 127 | 128 | if (!isset(self::$playerStream[$path])) { 129 | self::$playerStream[$path] = []; 130 | } 131 | 132 | self::$playerStream[$path][$objIndex] = $playerStream; 133 | 134 | if (self::hasPublishStream($path)) { 135 | $p_stream = self::getPublishStream($path); 136 | if (!$p_stream->is_on_frame) { 137 | $p_stream->on('on_frame', self::class . '::publisherOnFrame'); 138 | $p_stream->is_on_frame = true; 139 | } 140 | } 141 | 142 | } 143 | 144 | 145 | /** 146 | * @param $publisher PublishStreamInterface 147 | * @param $frame MediaFrame 148 | */ 149 | static function publisherOnFrame($frame, $publisher) 150 | { 151 | foreach (self::getPlayStreams($publisher->getPublishPath()) as $playStream) { 152 | if (!$playStream->isPlayerIdling()) { 153 | $playStream->frameSend($frame); 154 | } 155 | } 156 | } 157 | 158 | 159 | /** 160 | * 161 | * @param PublishStreamInterface $stream 162 | * @return mixed 163 | */ 164 | static public function addPublish($stream) 165 | { 166 | $path = $stream->getPublishPath(); 167 | $stream->is_on_frame = false; 168 | 169 | $stream->on('on_publish_ready', function () use ($path) { 170 | foreach (self::getPlayStreams($path) as $playStream) { 171 | if ($playStream->isPlayerIdling()) { 172 | $playStream->startPlay(); 173 | } 174 | } 175 | }); 176 | 177 | if (count(self::getPlayStreams($path)) > 0) { 178 | $stream->on('on_frame', self::class . '::publisherOnFrame'); 179 | $stream->is_on_frame = true; 180 | } 181 | 182 | 183 | $stream->on('on_close', function () use ($path) { 184 | foreach (self::getPlayStreams($path) as $playStream) { 185 | $playStream->playClose(); 186 | } 187 | 188 | self::delPublishStream($path); 189 | 190 | }); 191 | 192 | self::addPublishStream($stream); 193 | 194 | logger()->info(" add publisher {path}", ['path' => $path]); 195 | 196 | return true; 197 | 198 | } 199 | 200 | /** 201 | * @param PlayStreamInterface $playerStream 202 | */ 203 | static public function addPlayer($playerStream) 204 | { 205 | 206 | $objIndex = spl_object_id($playerStream); 207 | $path = $playerStream->getPlayPath(); 208 | 209 | //on close event 210 | $playerStream->on("on_close", function () use ($path, $objIndex) { 211 | //echo "play on close", PHP_EOL; 212 | self::delPlayerStream($path, $objIndex); 213 | }); 214 | 215 | self::addPlayerStream($playerStream); 216 | 217 | //判断当前是否有对应的推流设备 218 | if (self::hasPublishStream($path)) { 219 | $playerStream->startPlay(); 220 | } 221 | 222 | logger()->info(" add player {path}", ['path' => $path]); 223 | 224 | } 225 | 226 | /** 227 | * @param $stream VerifyAuthStreamInterface 228 | * @return bool 229 | */ 230 | static public function verifyAuth($stream) 231 | { 232 | return true; 233 | } 234 | 235 | 236 | } 237 | -------------------------------------------------------------------------------- /src/Rtmp/RtmpStream.php: -------------------------------------------------------------------------------- 1 | id = generateNewSessionID(); 177 | $this->handshakeState = RtmpHandshake::RTMP_HANDSHAKE_UNINIT; 178 | $this->ip = ''; 179 | $this->isStarting = true; 180 | $this->buffer = $bufferStream; 181 | $bufferStream->on('onData',[$this,'onStreamData']); 182 | $bufferStream->on('onError',[$this,'onStreamError']); 183 | $bufferStream->on('onClose',[$this,'onStreamClose']); 184 | 185 | /* 186 | * 统计数据量代码 187 | * 188 | */ 189 | $this->dataCountTimer = Timer::add(5,function(){ 190 | $avgTime=$this->frameTimeCount/($this->frameCount?:1); 191 | $avgPack=$this->frameCount/5; 192 | $packPs=(1/($avgTime?:1)); 193 | // $s=$packPs/$avgPack; 194 | $this->frameCount=0; 195 | $this->frameTimeCount=0; 196 | $this->bytesRead = $this->buffer->connection->bytesRead; 197 | $this->bytesReadRate = $this->bytesRead/ (timestamp() - $this->startTimestamp) * 1000; 198 | //logger()->info("[rtmp on data] {$packPs} pps {$avgPack} ps {$s} stream"); 199 | }); 200 | } 201 | 202 | public $dataCountTimer; 203 | public $frameCount = 0; 204 | public $frameTimeCount = 0; 205 | public $bytesRead = 0; 206 | public $bytesReadRate = 0; 207 | 208 | public function onStreamData() 209 | { 210 | //若干秒后没有收到数据断开 211 | $b = microtime(true); 212 | 213 | if ($this->handshakeState < RtmpHandshake::RTMP_HANDSHAKE_C2) { 214 | $this->onHandShake(); 215 | } 216 | 217 | if ($this->handshakeState === RtmpHandshake::RTMP_HANDSHAKE_C2) { 218 | $this->onChunkData(); 219 | 220 | $this->inAckSize += strlen($this->buffer->recvSize()); 221 | if ($this->inAckSize >= 0xf0000000) { 222 | $this->inAckSize = 0; 223 | $this->inLastAck = 0; 224 | } 225 | if ($this->ackSize > 0 && $this->inAckSize - $this->inLastAck >= $this->ackSize) { 226 | //每次收到的数据超过ack设的值 227 | $this->inLastAck = $this->inAckSize; 228 | $this->sendACK($this->inAckSize); 229 | } 230 | } 231 | $this->frameTimeCount += microtime(true) - $b; 232 | $this->frameCount++; 233 | 234 | 235 | //logger()->info("[rtmp on data] per sec handler times: ".(1/($end?:1))); 236 | } 237 | 238 | 239 | public function onStreamClose() 240 | { 241 | $this->stop(); 242 | } 243 | 244 | 245 | public function onStreamError() 246 | { 247 | $this->stop(); 248 | } 249 | 250 | 251 | public function write($data) 252 | { 253 | return $this->buffer->connection->send($data,true); 254 | } 255 | 256 | /* public function __destruct() 257 | { 258 | logger()->info("[RtmpStream __destruct] id={$this->id}"); 259 | }*/ 260 | 261 | 262 | 263 | } 264 | -------------------------------------------------------------------------------- /packages/SabreAMF/Client.php: -------------------------------------------------------------------------------- 1 | endPoint = $endPoint; 84 | 85 | $this->amfRequest = new SabreAMF_Message(); 86 | $this->amfOutputStream = new SabreAMF_OutputStream(); 87 | 88 | } 89 | 90 | 91 | /** 92 | * sendRequest 93 | * 94 | * sendRequest sends the request to the server. It expects the servicepath and methodname, and the parameters of the methodcall 95 | * 96 | * @param string $servicePath The servicepath (e.g.: myservice.mymethod) 97 | * @param array $data The parameters you want to send 98 | * @return mixed 99 | */ 100 | public function sendRequest($servicePath,$data) { 101 | 102 | // We're using the FLEX Messaging framework 103 | if($this->encoding & SabreAMF_Const::FLEXMSG) { 104 | 105 | 106 | // Setting up the message 107 | $message = new SabreAMF_AMF3_RemotingMessage(); 108 | $message->body = $data; 109 | 110 | // We need to split serviceName.methodName into separate variables 111 | $service = explode('.',$servicePath); 112 | $method = array_pop($service); 113 | $service = implode('.',$service); 114 | $message->operation = $method; 115 | $message->source = $service; 116 | 117 | $data = $message; 118 | } 119 | 120 | $this->amfRequest->addBody(array( 121 | 122 | // If we're using the flex messaging framework, target is specified as the string 'null' 123 | 'target' => $this->encoding & SabreAMF_Const::FLEXMSG?'null':$servicePath, 124 | 'response' => '/1', 125 | 'data' => $data 126 | )); 127 | 128 | $this->amfRequest->serialize($this->amfOutputStream); 129 | 130 | // The curl request 131 | $ch = curl_init($this->endPoint); 132 | curl_setopt($ch,CURLOPT_POST,1); 133 | curl_setopt($ch,CURLOPT_RETURNTRANSFER, 1); 134 | curl_setopt($ch,CURLOPT_TIMEOUT,20); 135 | curl_setopt($ch,CURLOPT_HTTPHEADER,array('Content-type: ' . SabreAMF_Const::MIMETYPE)); 136 | curl_setopt($ch,CURLOPT_POSTFIELDS,$this->amfOutputStream->getRawData()); 137 | if ($this->httpProxy) { 138 | curl_setopt($ch,CURLOPT_PROXY,$this->httpProxy); 139 | } 140 | $result = curl_exec($ch); 141 | 142 | if (curl_errno($ch)) { 143 | throw new Exception('CURL error: ' . curl_error($ch)); 144 | false; 145 | } else { 146 | curl_close($ch); 147 | } 148 | 149 | $this->amfInputStream = new SabreAMF_InputStream($result); 150 | $this->amfResponse = new SabreAMF_Message(); 151 | $this->amfResponse->deserialize($this->amfInputStream); 152 | 153 | $this->parseHeaders(); 154 | 155 | foreach($this->amfResponse->getBodies() as $body) { 156 | 157 | if (strpos($body['target'],'/1')===0) return $body['data'] ; 158 | 159 | } 160 | 161 | } 162 | 163 | /** 164 | * addHeader 165 | * 166 | * Add a header to the client request 167 | * 168 | * @param string $name 169 | * @param bool $required 170 | * @param mixed $data 171 | * @return void 172 | */ 173 | public function addHeader($name,$required,$data) { 174 | 175 | $this->amfRequest->addHeader(array('name'=>$name,'required'=>$required==true,'data'=>$data)); 176 | 177 | } 178 | 179 | /** 180 | * setCredentials 181 | * 182 | * @param string $username 183 | * @param string $password 184 | * @return void 185 | */ 186 | public function setCredentials($username,$password) { 187 | 188 | $this->addHeader('Credentials',false,(object)array('userid'=>$username,'password'=>$password)); 189 | 190 | } 191 | 192 | /** 193 | * setHttpProxy 194 | * 195 | * @param mixed $httpProxy 196 | * @return void 197 | */ 198 | public function setHttpProxy($httpProxy) { 199 | $this->httpProxy = $httpProxy; 200 | } 201 | 202 | /** 203 | * parseHeaders 204 | * 205 | * @return void 206 | */ 207 | private function parseHeaders() { 208 | 209 | foreach($this->amfResponse->getHeaders() as $header) { 210 | 211 | switch($header['name']) { 212 | 213 | case 'ReplaceGatewayUrl' : 214 | if (is_string($header['data'])) { 215 | $this->endPoint = $header['data']; 216 | } 217 | break; 218 | 219 | } 220 | 221 | 222 | } 223 | 224 | } 225 | 226 | /** 227 | * Change the AMF encoding (0 or 3) 228 | * 229 | * @param int $encoding 230 | * @return void 231 | */ 232 | public function setEncoding($encoding) { 233 | 234 | $this->encoding = $encoding; 235 | $this->amfRequest->setEncoding($encoding & SabreAMF_Const::AMF3); 236 | 237 | } 238 | 239 | } 240 | 241 | 242 | 243 | -------------------------------------------------------------------------------- /packages/SabreAMF/Server.php: -------------------------------------------------------------------------------- 1 | readInput(); 77 | 78 | //file_put_contents($dump.'/' . md5($data),$data); 79 | 80 | $this->amfInputStream = new SabreAMF_InputStream($data); 81 | 82 | $this->amfRequest = new SabreAMF_Message(); 83 | $this->amfOutputStream = new SabreAMF_OutputStream(); 84 | $this->amfResponse = new SabreAMF_Message(); 85 | 86 | $this->amfRequest->deserialize($this->amfInputStream); 87 | 88 | } 89 | 90 | /** 91 | * getRequests 92 | * 93 | * Returns the requests that are made to the gateway. 94 | * 95 | * @return array 96 | */ 97 | public function getRequests() { 98 | 99 | return $this->amfRequest->getBodies(); 100 | 101 | } 102 | 103 | /** 104 | * setResponse 105 | * 106 | * Send a response back to the client (based on a request you got through getRequests) 107 | * 108 | * @param string $target This parameter should contain the same as the 'response' item you got through getRequests. This connects the request to the response 109 | * @param int $responsetype Set as either SabreAMF_Const::R_RESULT or SabreAMF_Const::R_STATUS, depending on if the call succeeded or an error was produced 110 | * @param mixed $data The result data 111 | * @return void 112 | */ 113 | public function setResponse($target,$responsetype,$data) { 114 | 115 | 116 | switch($responsetype) { 117 | 118 | case SabreAMF_Const::R_RESULT : 119 | $target = $target.='/onResult'; 120 | break; 121 | case SabreAMF_Const::R_STATUS : 122 | $target = $target.='/onStatus'; 123 | break; 124 | case SabreAMF_Const::R_DEBUG : 125 | $target = '/onDebugEvents'; 126 | break; 127 | } 128 | return $this->amfResponse->addBody(array('target'=>$target,'response'=>'','data'=>$data)); 129 | 130 | } 131 | 132 | /** 133 | * sendResponse 134 | * 135 | * Sends the responses back to the client. Call this after you answered all the requests with setResponse 136 | * 137 | * @return void 138 | */ 139 | public function sendResponse() { 140 | 141 | header('Content-Type: ' . SabreAMF_Const::MIMETYPE); 142 | $this->amfResponse->setEncoding($this->amfRequest->getEncoding()); 143 | $this->amfResponse->serialize($this->amfOutputStream); 144 | echo($this->amfOutputStream->getRawData()); 145 | 146 | } 147 | 148 | /** 149 | * addHeader 150 | * 151 | * Add a header to the server response 152 | * 153 | * @param string $name 154 | * @param bool $required 155 | * @param mixed $data 156 | * @return void 157 | */ 158 | public function addHeader($name,$required,$data) { 159 | 160 | $this->amfResponse->addHeader(array('name'=>$name,'required'=>$required==true,'data'=>$data)); 161 | 162 | } 163 | 164 | /** 165 | * getRequestHeaders 166 | * 167 | * returns the request headers 168 | * 169 | * @return void 170 | */ 171 | public function getRequestHeaders() { 172 | 173 | return $this->amfRequest->getHeaders(); 174 | 175 | } 176 | 177 | /** 178 | * setInputFile 179 | * 180 | * returns the true/false depended on wheater the stream is readable 181 | * 182 | * @param string $stream New input stream 183 | * 184 | * @author Asbjørn Sloth Tønnesen 185 | * @return bool 186 | */ 187 | static public function setInputFile($stream) { 188 | 189 | if (!is_readable($stream)) return false; 190 | 191 | self::$dataInputStream = $stream; 192 | return true; 193 | 194 | } 195 | 196 | /** 197 | * setInputString 198 | * 199 | * Returns the true/false depended on wheater the string was accepted. 200 | * That a string is accepted by this method, does NOT mean that it is a valid AMF request. 201 | * 202 | * @param string $string New input string 203 | * 204 | * @author Asbjørn Sloth Tønnesen 205 | * @return bool 206 | */ 207 | static public function setInputString($string) { 208 | 209 | if (!(is_string($string) && strlen($string) > 0)) 210 | throw new SabreAMF_InvalidAMFException(); 211 | 212 | self::$dataInputStream = null; 213 | self::$dataInputData = $string; 214 | return true; 215 | 216 | } 217 | 218 | /** 219 | * readInput 220 | * 221 | * Reads the input from stdin unless it has been overwritten 222 | * with setInputFile or setInputString. 223 | * 224 | * @author Asbjørn Sloth Tønnesen 225 | * @return string Binary string containing the AMF data 226 | */ 227 | protected function readInput() { 228 | 229 | if (is_null(self::$dataInputStream)) return self::$dataInputData; 230 | 231 | $data = file_get_contents(self::$dataInputStream); 232 | if (!$data) throw new SabreAMF_InvalidAMFException(); 233 | 234 | return $data; 235 | 236 | } 237 | 238 | } 239 | 240 | 241 | -------------------------------------------------------------------------------- /src/Http/HttpWMServer.php: -------------------------------------------------------------------------------- 1 | onWebSocketConnect = [$this,'onWebsocketRequest']; 28 | $this->onMessage = [$this,'onHttpRequest']; 29 | } 30 | 31 | public function onWebsocketRequest($connection,$headerData){ 32 | $request = new Request($headerData); 33 | $request->connection = $connection; 34 | //ignore connection message 35 | $connection->onMessage = null; 36 | if($this->findFlv($request,$request->path())){ 37 | return; 38 | } 39 | $request->connection->close(); 40 | return; 41 | } 42 | 43 | 44 | /** 45 | * @param $connection TcpConnection 46 | * @param Request $request 47 | */ 48 | public function onHttpRequest($connection,Request $request) 49 | { 50 | switch ($request->method()) { 51 | case "GET": 52 | return $this->getHandler($request); 53 | case "POST": 54 | return $this->postHandler($request); 55 | case "HEAD": 56 | return $connection->send(new \Workerman\Protocols\Http\Response(200)); 57 | default: 58 | logger()->warning("unknown method", ['method' => $request->method(), 'path' => $request->path()]); 59 | return $connection->send(new \Workerman\Protocols\Http\Response(405)); 60 | } 61 | } 62 | 63 | 64 | /** 65 | * @param Request $request 66 | */ 67 | public function getHandler(Request $request) 68 | { 69 | $path = $request->path(); 70 | 71 | //api 72 | if($path ==='/api'){ 73 | $name = $request->get('name'); 74 | $args = $request->get('args',[]); 75 | $data = MediaServer::callApi($name,$args); 76 | if(!is_null($data)){ 77 | $request->connection->send(new Response(200,['Content-Type'=>"application/json"],json_encode($data))); 78 | }else{ 79 | $request->connection->send(new Response(404,[],'404 Not Found')); 80 | } 81 | return; 82 | } 83 | //flv 84 | if( 85 | $this->unsafeUri($request,$path) || 86 | $this->findFlv($request,$path) || 87 | $this->findStaticFile($request,$path) 88 | ){ 89 | return; 90 | } 91 | 92 | //api 93 | 94 | //404 95 | $request->connection->send(new Response(404,[],'404 Not Found')); 96 | return; 97 | } 98 | 99 | 100 | 101 | /** 102 | * @param Request $request 103 | * @return Promise|Response 104 | */ 105 | public function postHandler(Request $request) 106 | { 107 | $path = $request->getUri()->getPath(); 108 | $bodyStream = $request->getBody(); 109 | if(!$bodyStream instanceof StreamInterface || !$bodyStream instanceof ReadableStreamInterface){ 110 | return new Response( 111 | 500, 112 | ['Content-Type' => 'text/plain'], 113 | "Stream error." 114 | ); 115 | }; 116 | 117 | if (MediaServer::hasPublishStream($path)) { 118 | //publishStream already 119 | logger()->warning("Stream {path} exists", ['path' => $path]); 120 | return new Response( 121 | 400, 122 | ['Content-Type' => 'text/plain'], 123 | "Stream {$path} exists." 124 | ); 125 | } 126 | 127 | 128 | return new Promise(function ($resolve, $reject) use ($bodyStream, $path) { 129 | $flvReadStream = new FlvPublisherStream( 130 | $bodyStream, 131 | $path 132 | ); 133 | 134 | MediaServer::addPublish($flvReadStream); 135 | logger()->info("stream {path} created", ['path' => $path]); 136 | $flvReadStream->on('on_end', function () use ($resolve) { 137 | $resolve(new Response(200)); 138 | }); 139 | $flvReadStream->on('error', function (\Exception $exception) use ($reject, &$bytes) { 140 | $reject(new Response( 141 | 400, 142 | ['Content-Type' => 'text/plain'], 143 | $exception->getMessage() 144 | )); 145 | }); 146 | }); 147 | } 148 | 149 | public function unsafeUri(Request $request,$path): bool 150 | { 151 | if ( 152 | !$path || 153 | strpos($path, '..') !== false || 154 | strpos($path, "\\") !== false || 155 | strpos($path, "\0") !== false 156 | ) { 157 | $request->connection->send(new Response(404,[],'404 Not Found.')); 158 | return true; 159 | } 160 | return false; 161 | } 162 | 163 | public function findStaticFile(Request $request,$path){ 164 | 165 | if (preg_match('/%[0-9a-f]{2}/i', $path)) { 166 | $path = urldecode($path); 167 | if ($this->unsafeUri($request,$path)) { 168 | return true; 169 | } 170 | } 171 | 172 | $file = self::$publicPath."/$path"; 173 | if (!is_file($file)) { 174 | return false; 175 | } 176 | 177 | $request->connection->send((new Response())->withFile($file)); 178 | 179 | return true; 180 | } 181 | 182 | public function findFlv(Request $request,$path){ 183 | if(!preg_match('/(.*)\.flv$/',$path,$matches)){ 184 | return false; 185 | }else{ 186 | list(,$flvPath) = $matches; 187 | $this->playMediaStream($request,$flvPath); 188 | return true; 189 | } 190 | } 191 | 192 | 193 | public function playMediaStream(Request $request,$flvPath){ 194 | //check stream 195 | if (MediaServer::hasPublishStream($flvPath)) { 196 | 197 | if($request->connection->protocol === Websocket::class){ 198 | $request->connection->websocketType = Websocket::BINARY_TYPE_ARRAYBUFFER; 199 | $throughStream = new WMWsChunkStream($request->connection); 200 | }else{ 201 | $throughStream = new WMHttpChunkStream($request->connection); 202 | } 203 | $playerStream = new FlvPlayStream($throughStream, $flvPath); 204 | 205 | $disableAudio = $request->get('disableAudio',false); 206 | if ($disableAudio) { 207 | $playerStream->setEnableAudio(false); 208 | } 209 | 210 | $disableVideo = $request->get('disableVideo', false); 211 | if ($disableVideo) { 212 | $playerStream->setEnableVideo(false); 213 | } 214 | 215 | $disableGop = $request->get('disableGop', false); 216 | if ($disableGop) { 217 | $playerStream->setEnableGop(false); 218 | } 219 | MediaServer::addPlayer($playerStream); 220 | } else { 221 | logger()->warning("Stream {path} not found", ['path' => $flvPath]); 222 | if($request->connection->protocol === Websocket::class){ 223 | $request->connection->close(); 224 | }else{ 225 | $request->connection->send(new Response(404, 226 | ['Content-Type' => 'text/plain'], 227 | "Stream not found.")); 228 | } 229 | 230 | } 231 | } 232 | 233 | 234 | } -------------------------------------------------------------------------------- /packages/SabreAMF/Message.php: -------------------------------------------------------------------------------- 1 | outputStream = $stream; 61 | $stream->writeByte(0x00); 62 | $stream->writeByte($this->encoding); 63 | $stream->writeInt(count($this->headers)); 64 | 65 | foreach($this->headers as $header) { 66 | 67 | $serializer = new SabreAMF_AMF0_Serializer($stream); 68 | $serializer->writeString($header['name']); 69 | $stream->writeByte($header['required']==true); 70 | $stream->writeLong(-1); 71 | $serializer->writeAMFData($header['data']); 72 | } 73 | 74 | $stream->writeInt(count($this->bodies)); 75 | 76 | 77 | foreach($this->bodies as $body) { 78 | $serializer = new SabreAMF_AMF0_Serializer($stream); 79 | $serializer->writeString($body['target']); 80 | $serializer->writeString($body['response']); 81 | $stream->writeLong(-1); 82 | 83 | switch($this->encoding) { 84 | 85 | case SabreAMF_Const::AMF0 : 86 | $serializer->writeAMFData($body['data']); 87 | break; 88 | case SabreAMF_Const::AMF3 : 89 | $serializer->writeAMFData(new SabreAMF_AMF3_Wrapper($body['data'])); 90 | break; 91 | 92 | } 93 | 94 | } 95 | 96 | } 97 | 98 | /** 99 | * deserialize 100 | * 101 | * This method deserializes a request. It requires an SabreAMF_InputStream with valid AMF data. After 102 | * deserialization the contents of the request can be found through the getBodies and getHeaders methods 103 | * 104 | * @param SabreAMF_InputStream $stream 105 | * @return void 106 | */ 107 | public function deserialize(SabreAMF_InputStream $stream) { 108 | 109 | $this->headers = array(); 110 | $this->bodies = array(); 111 | 112 | $this->InputStream = $stream; 113 | 114 | $stream->readByte(); 115 | 116 | $this->clientType = $stream->readByte(); 117 | 118 | $deserializer = new SabreAMF_AMF0_Deserializer($stream); 119 | 120 | $totalHeaders = $stream->readInt(); 121 | 122 | for($i=0;$i<$totalHeaders;$i++) { 123 | 124 | $header = array( 125 | 'name' => $deserializer->readString(), 126 | 'required' => $stream->readByte()==true 127 | ); 128 | $stream->readLong(); 129 | $header['data'] = $deserializer->readAMFData(null,true); 130 | $this->headers[] = $header; 131 | 132 | } 133 | 134 | $totalBodies = $stream->readInt(); 135 | 136 | for($i=0;$i<$totalBodies;$i++) { 137 | 138 | try { 139 | $target = $deserializer->readString(); 140 | } catch (Exception $e) { 141 | // Could not fetch next body.. this happens with some versions of AMFPHP where the body 142 | // count isn't properly set. If this happens we simply stop decoding 143 | break; 144 | } 145 | 146 | $body = array( 147 | 'target' => $target, 148 | 'response' => $deserializer->readString(), 149 | 'length' => $stream->readLong(), 150 | 'data' => $deserializer->readAMFData(null,true) 151 | ); 152 | 153 | if (is_object($body['data']) && $body['data'] instanceof SabreAMF_AMF3_Wrapper) { 154 | $body['data'] = $body['data']->getData(); 155 | $this->encoding = SabreAMF_Const::AMF3; 156 | } else if (is_array($body['data']) && isset($body['data'][0]) && is_object($body['data'][0]) && $body['data'][0] instanceof SabreAMF_AMF3_Wrapper) { 157 | $body['data'] = $body['data'][0]->getData(); 158 | $this->encoding = SabreAMF_Const::AMF3; 159 | } 160 | 161 | $this->bodies[] = $body; 162 | 163 | } 164 | 165 | 166 | } 167 | 168 | /** 169 | * getClientType 170 | * 171 | * Returns the ClientType for the request. Check SabreAMF_Const for possible (known) values 172 | * 173 | * @return int 174 | */ 175 | public function getClientType() { 176 | 177 | return $this->clientType; 178 | 179 | } 180 | 181 | /** 182 | * getBodies 183 | * 184 | * Returns the bodies int the message 185 | * 186 | * @return array 187 | */ 188 | public function getBodies() { 189 | 190 | return $this->bodies; 191 | 192 | } 193 | 194 | /** 195 | * getHeaders 196 | * 197 | * Returns the headers in the message 198 | * 199 | * @return array 200 | */ 201 | public function getHeaders() { 202 | 203 | return $this->headers; 204 | 205 | } 206 | 207 | /** 208 | * addBody 209 | * 210 | * Adds a body to the message 211 | * 212 | * @param mixed $body 213 | * @return void 214 | */ 215 | public function addBody($body) { 216 | 217 | $this->bodies[] = $body; 218 | 219 | } 220 | 221 | /** 222 | * addHeader 223 | * 224 | * Adds a message header 225 | * 226 | * @param mixed $header 227 | * @return void 228 | */ 229 | public function addHeader($header) { 230 | 231 | $this->headers[] = $header; 232 | 233 | } 234 | 235 | /** 236 | * setEncoding 237 | * 238 | * @param int $encoding 239 | * @return void 240 | */ 241 | public function setEncoding($encoding) { 242 | 243 | $this->encoding = $encoding; 244 | 245 | } 246 | 247 | /** 248 | * getEncoding 249 | * 250 | * @return int 251 | */ 252 | public function getEncoding() { 253 | 254 | return $this->encoding; 255 | 256 | } 257 | 258 | } 259 | 260 | 261 | -------------------------------------------------------------------------------- /packages/SabreAMF/AMF0/Deserializer.php: -------------------------------------------------------------------------------- 1 | refList = array(); 51 | 52 | if (is_null($settype)) { 53 | $settype = $this->stream->readByte(); 54 | } 55 | 56 | switch ($settype) { 57 | 58 | case SabreAMF_AMF0_Const::DT_NUMBER : return $this->stream->readDouble(); 59 | case SabreAMF_AMF0_Const::DT_BOOL : return $this->stream->readByte()==true; 60 | case SabreAMF_AMF0_Const::DT_STRING : return $this->readString(); 61 | case SabreAMF_AMF0_Const::DT_OBJECT : return $this->readObject(); 62 | case SabreAMF_AMF0_Const::DT_NULL : return null; 63 | case SabreAMF_AMF0_Const::DT_UNDEFINED : return null; 64 | case SabreAMF_AMF0_Const::DT_REFERENCE : return $this->readReference(); 65 | case SabreAMF_AMF0_Const::DT_MIXEDARRAY : return $this->readMixedArray(); 66 | case SabreAMF_AMF0_Const::DT_ARRAY : return $this->readArray(); 67 | case SabreAMF_AMF0_Const::DT_DATE : return $this->readDate(); 68 | case SabreAMF_AMF0_Const::DT_LONGSTRING : return $this->readLongString(); 69 | case SabreAMF_AMF0_Const::DT_UNSUPPORTED : return null; 70 | case SabreAMF_AMF0_Const::DT_XML : return $this->readLongString(); 71 | case SabreAMF_AMF0_Const::DT_TYPEDOBJECT : return $this->readTypedObject(); 72 | case SabreAMF_AMF0_Const::DT_AMF3 : return $this->readAMF3Data(); 73 | default : throw new Exception('Unsupported type: 0x' . strtoupper(str_pad(dechex($settype),2,0,STR_PAD_LEFT))); return false; 74 | 75 | } 76 | 77 | } 78 | 79 | /** 80 | * readObject 81 | * 82 | * @return object 83 | */ 84 | public function readObject() { 85 | 86 | $object = array(); 87 | $this->refList[] =& $object; 88 | while (true) { 89 | $key = $this->readString(); 90 | $vartype = $this->stream->readByte(); 91 | if ($vartype==SabreAMF_AMF0_Const::DT_OBJECTTERM) break; 92 | $object[$key] = $this->readAmfData($vartype); 93 | } 94 | if (defined('SABREAMF_OBJECT_AS_ARRAY')) { 95 | $object = (object)$object; 96 | } 97 | return $object; 98 | 99 | } 100 | 101 | /** 102 | * readReference 103 | * 104 | * @return object 105 | */ 106 | public function readReference() { 107 | 108 | $refId = $this->stream->readInt(); 109 | if (isset($this->refList[$refId])) { 110 | return $this->refList[$refId]; 111 | } else { 112 | throw new Exception('Invalid reference offset: ' . $refId); 113 | return false; 114 | } 115 | 116 | } 117 | 118 | 119 | /** 120 | * readArray 121 | * 122 | * @return array 123 | */ 124 | public function readArray() { 125 | 126 | $length = $this->stream->readLong(); 127 | $arr = array(); 128 | $this->refList[]&=$arr; 129 | while($length--) $arr[] = $this->readAMFData(); 130 | return $arr; 131 | 132 | } 133 | 134 | /** 135 | * readMixedArray 136 | * 137 | * @return array 138 | */ 139 | public function readMixedArray() { 140 | 141 | $highestIndex = $this->stream->readLong(); 142 | return $this->readObject(); 143 | 144 | } 145 | 146 | /** 147 | * readString 148 | * 149 | * @return string 150 | */ 151 | public function readString() { 152 | 153 | $strLen = $this->stream->readInt(); 154 | return $this->stream->readBuffer($strLen); 155 | 156 | } 157 | 158 | /** 159 | * readLongString 160 | * 161 | * @return string 162 | */ 163 | public function readLongString() { 164 | 165 | $strLen = $this->stream->readLong(); 166 | return $this->stream->readBuffer($strLen); 167 | 168 | } 169 | 170 | /** 171 | * 172 | * readDate 173 | * 174 | * @return int 175 | */ 176 | public function readDate() { 177 | 178 | // Unix timestamp in seconds. We strip the millisecond part 179 | $timestamp = floor($this->stream->readDouble() / 1000); 180 | 181 | // we are ignoring the timezone 182 | $timezoneOffset = $this->stream->readInt(); 183 | //if ($timezoneOffset > 720) $timezoneOffset = ((65536 - $timezoneOffset)); 184 | //$timezoneOffset=($timezoneOffset * 60) - date('Z'); 185 | 186 | $dateTime = new DateTime('@' . $timestamp); 187 | 188 | return $dateTime; 189 | 190 | } 191 | 192 | /** 193 | * readTypedObject 194 | * 195 | * @return object 196 | */ 197 | public function readTypedObject() { 198 | 199 | $classname = $this->readString(); 200 | 201 | $isMapped = false; 202 | 203 | if ($classname = $this->getLocalClassName($classname)) { 204 | $rObject = new $classname(); 205 | $isMapped = true; 206 | } else { 207 | $rObject = new SabreAMF_TypedObject($classname,null); 208 | } 209 | $this->refList[] =& $rObject; 210 | 211 | $props = array(); 212 | while (true) { 213 | $key = $this->readString(); 214 | $vartype = $this->stream->readByte(); 215 | if ($vartype==SabreAMF_AMF0_Const::DT_OBJECTTERM) break; 216 | $props[$key] = $this->readAmfData($vartype); 217 | } 218 | 219 | if ($isMapped) { 220 | foreach($props as $k=>$v) 221 | $rObject->$k = $v; 222 | } else { 223 | $rObject->setAMFData($props); 224 | } 225 | 226 | return $rObject; 227 | 228 | } 229 | 230 | /** 231 | * readAMF3Data 232 | * 233 | * @return SabreAMF_AMF3_Wrapper 234 | */ 235 | public function readAMF3Data() { 236 | 237 | $amf3Deserializer = new SabreAMF_AMF3_Deserializer($this->stream); 238 | return new SabreAMF_AMF3_Wrapper($amf3Deserializer->readAMFData()); 239 | 240 | } 241 | 242 | 243 | } 244 | 245 | 246 | -------------------------------------------------------------------------------- /src/Rtmp/RtmpChunkHandlerTrait.php: -------------------------------------------------------------------------------- 1 | buffer; 25 | 26 | switch ($this->chunkState) { 27 | case RtmpChunk::CHUNK_STATE_BEGIN: 28 | if ($stream->has(1)) { 29 | 30 | $stream->tag(); 31 | $header = $stream->readTinyInt(); 32 | $stream->rollBack(); 33 | 34 | $chunkHeaderLen = RtmpChunk::BASE_HEADER_SIZES[$header & 0x3f] ?? 1; //base header size 35 | //logger()->info('base header size ' . $chunkHeaderLen); 36 | $chunkHeaderLen += RtmpChunk::MSG_HEADER_SIZES[$header >> 6]; //messaege header size 37 | //logger()->info('base + msg header size ' . $chunkHeaderLen); 38 | //base header + message header 39 | $this->chunkHeaderLen = $chunkHeaderLen; 40 | $this->chunkState = RtmpChunk::CHUNK_STATE_HEADER_READY; 41 | 42 | } else { 43 | break; 44 | } 45 | case RtmpChunk::CHUNK_STATE_HEADER_READY: 46 | if ($stream->has($this->chunkHeaderLen)) { 47 | //get base header + message header 48 | $header = $stream->readTinyInt(); 49 | $fmt = $header >> 6; 50 | switch ($csId = $header & 0x3f) { 51 | case 0: 52 | $csId = $stream->readTinyInt() + 64; 53 | break; 54 | case 1: 55 | //小端 56 | $csId = 64 + $stream->readInt16LE(); 57 | break; 58 | } 59 | 60 | //logger()->info("header ready fmt {$fmt} csid {$csId}"); 61 | //找出当前的流所属的包 62 | if (!isset($this->allPackets[$csId])) { 63 | logger()->info("new packet csid {$csId}"); 64 | $p = new RtmpPacket(); 65 | $p->chunkStreamId = $csId; 66 | $p->baseHeaderLen = RtmpChunk::BASE_HEADER_SIZES[$csId] ?? 1; 67 | $this->allPackets[$csId] = $p; 68 | } else { 69 | //logger()->info("old packet csid {$csId}"); 70 | $p = $this->allPackets[$csId]; 71 | } 72 | 73 | //set fmt 74 | $p->chunkType = $fmt; 75 | //更新长度数据 76 | $p->chunkHeaderLen = $this->chunkHeaderLen; 77 | 78 | //base header 长度不变 79 | //$p->baseHeaderLen = RtmpPacket::$BASEHEADERSIZE[$csId] ?? 1; 80 | $p->msgHeaderLen = $p->chunkHeaderLen - $p->baseHeaderLen; 81 | 82 | //logger()->info("packet chunkheaderLen {$p->chunkHeaderLen} msg header len {$p->msgHeaderLen}"); 83 | //当前包 84 | $this->currentPacket = $p; 85 | $this->chunkState = RtmpChunk::CHUNK_STATE_CHUNK_READY; 86 | 87 | if ($p->chunkType === RtmpChunk::CHUNK_TYPE_3) { 88 | //直接进入判断是否需要读取扩展时间戳的流程 89 | $p->state = RtmpPacket::PACKET_STATE_EXT_TIMESTAMP; 90 | } else { 91 | //当前包的状态初始化 92 | $p->state = RtmpPacket::PACKET_STATE_MSG_HEADER; 93 | 94 | } 95 | } else { 96 | break; 97 | } 98 | case RtmpChunk::CHUNK_STATE_CHUNK_READY: 99 | if (false === $this->onPacketHandler()) { 100 | break; 101 | } 102 | default: 103 | //跑一下看看剩余的数据够不够 104 | $this->onChunkData(); 105 | break; 106 | } 107 | 108 | 109 | } 110 | 111 | 112 | /** 113 | * @param $packet 114 | * @return string 115 | */ 116 | public function rtmpChunksCreate(&$packet) 117 | { 118 | $baseHeader = $this->rtmpChunkBasicHeaderCreate($packet->chunkType, $packet->chunkStreamId); 119 | $baseHeader3 = $this->rtmpChunkBasicHeaderCreate(RtmpChunk::CHUNK_TYPE_3, $packet->chunkStreamId); 120 | 121 | $msgHeader = $this->rtmpChunkMessageHeaderCreate($packet); 122 | 123 | $useExtendedTimestamp = $packet->timestamp >= RtmpPacket::MAX_TIMESTAMP; 124 | 125 | $timestampBin = pack('N', $packet->timestamp); 126 | $out = $baseHeader . $msgHeader; 127 | if ($useExtendedTimestamp) { 128 | $out .= $timestampBin; 129 | } 130 | 131 | //读取payload 132 | $readOffset = 0; 133 | $chunkSize = $this->outChunkSize; 134 | while ($remain = $packet->length - $readOffset) { 135 | 136 | $size = min($remain, $chunkSize); 137 | //logger()->debug("rtmpChunksCreate remain {$remain} size {$size}"); 138 | $out .= substr($packet->payload, $readOffset, $size); 139 | $readOffset += $size; 140 | if ($readOffset < $packet->length) { 141 | //payload 还没读取完 142 | $out .= $baseHeader3; 143 | if ($useExtendedTimestamp) { 144 | $out .= $timestampBin; 145 | } 146 | } 147 | 148 | } 149 | 150 | return $out; 151 | } 152 | 153 | 154 | /** 155 | * @param $fmt 156 | * @param $cid 157 | */ 158 | public function rtmpChunkBasicHeaderCreate($fmt, $cid) 159 | { 160 | if ($cid >= 64 + 255) { 161 | //cid 小端字节序 162 | return pack('CS', $fmt << 6 | 1, $cid - 64); 163 | } elseif ($cid >= 64) { 164 | return pack('CC', $fmt << 6 | 0, $cid - 64); 165 | } else { 166 | return pack('C', $fmt << 6 | $cid); 167 | } 168 | } 169 | 170 | 171 | /** 172 | * @param $packet RtmpPacket 173 | */ 174 | public function rtmpChunkMessageHeaderCreate($packet) 175 | { 176 | $out = ""; 177 | if ($packet->chunkType <= RtmpChunk::CHUNK_TYPE_2) { 178 | //timestamp 179 | $out .= substr(pack('N', $packet->timestamp >= RtmpPacket::MAX_TIMESTAMP ? RtmpPacket::MAX_TIMESTAMP : $packet->timestamp), 1, 3); 180 | } 181 | 182 | if ($packet->chunkType <= RtmpChunk::CHUNK_TYPE_1) { 183 | //payload len and stream type 184 | $out .= substr(pack('N', $packet->length), 1, 3); 185 | //stream type 186 | $out .= pack('C', $packet->type); 187 | } 188 | 189 | if ($packet->chunkType == RtmpChunk::CHUNK_TYPE_0) { 190 | //stream id 小端字节序 191 | $out .= pack('L', $packet->streamId); 192 | } 193 | 194 | //logger()->debug("rtmpChunkMessageHeaderCreate " . bin2hex($out)); 195 | 196 | return $out; 197 | } 198 | 199 | 200 | public function sendACK($size) 201 | { 202 | $buf = hex2bin('02000000000004030000000000000000'); 203 | $buf = substr_replace($buf, pack('N', $size), 12); 204 | $this->write($buf); 205 | } 206 | 207 | public function sendWindowACK($size) 208 | { 209 | $buf = hex2bin('02000000000004050000000000000000'); 210 | $buf = substr_replace($buf, pack('N', $size), 12); 211 | $this->write($buf); 212 | } 213 | 214 | public function setPeerBandwidth($size, $type) 215 | { 216 | $buf = hex2bin('0200000000000506000000000000000000'); 217 | $buf = substr_replace($buf, pack('NC', $size, $type), 12); 218 | $this->write($buf); 219 | 220 | } 221 | 222 | public function setChunkSize($size) 223 | { 224 | $buf = hex2bin('02000000000004010000000000000000'); 225 | $buf = substr_replace($buf, pack('N', $size), 12); 226 | $this->write($buf); 227 | } 228 | 229 | 230 | } 231 | -------------------------------------------------------------------------------- /src/MediaReader/AVCSequenceParameterSet.php: -------------------------------------------------------------------------------- 1 | readData(); 20 | } 21 | 22 | 23 | /** 24 | * read unsigned exp golomb code. 25 | * 26 | * @return int 27 | */ 28 | protected function expGolombUe() 29 | { 30 | for ($n = 0; $this->getBit() == 0 && !$this->isError; $n++) ; 31 | return (1 << $n) + $this->getBits($n) - 1; 32 | } 33 | 34 | 35 | 36 | public function getAVCProfileName() 37 | { 38 | switch ($this->profile) { 39 | case 1: 40 | return 'Main'; 41 | case 2: 42 | return 'Main 10'; 43 | case 3: 44 | return 'Main Still Picture'; 45 | case 66: 46 | return 'Baseline'; 47 | case 77: 48 | return 'Main'; 49 | case 100: 50 | return 'High'; 51 | default: 52 | return ''; 53 | } 54 | } 55 | 56 | public function readData() 57 | { 58 | /*$data = []; 59 | $data['version'] = ord($this->data[$this->currentBytes++]); 60 | $data['profile'] = ord($this->data[$this->currentBytes++]); 61 | $data['profileCompatibility'] = ord($this->data[$this->currentBytes++]); 62 | $data['level'] = ord($this->data[$this->currentBytes++]); 63 | $data['naluSize'] = (ord($this->data[$this->currentBytes++]) & 0x03) + 1; 64 | $data['nbSps'] = ord($this->data[$this->currentBytes++]) & 0x1F; 65 | 66 | $data['sps'] = []; 67 | for ($i = 0; $i < $data['nbSps']; $i++) { 68 | //读取sps 69 | $len = (ord($this->data[$this->currentBytes++]) << 8) | ord($this->data[$this->currentBytes++]); 70 | var_dump(bin2hex(substr($this->data, $this->currentBytes, $len))); 71 | //var_dump(base64_encode(substr($this->data, $this->currentBytes, $len))); 72 | $byteTmp=$this->currentBytes; 73 | var_dump(bin2hex($this->data[$this->currentBytes])); 74 | 75 | $nalType=ord($this->data[$this->currentBytes++]) & 0x1f; 76 | 77 | if($nalType !== 0x07){ 78 | continue; 79 | } 80 | $sps=[]; 81 | $sps['nalType']=$nalType; 82 | $sps['profileIdc']=ord($this->data[$this->currentBytes++]); 83 | $sps['flags']=ord($this->data[$this->currentBytes++]); 84 | $sps['levelIdc']=ord($this->data[$this->currentBytes++]); 85 | 86 | $data['sps'][] = $sps; 87 | $this->currentBytes = $byteTmp+$len; 88 | } 89 | 90 | $data['nbPps'] = ord($this->data[$this->currentBytes++]); 91 | $data['pps'] = []; 92 | for ($i = 0; $i < $data['nbPps']; $i++) { 93 | //读取sps 94 | $len = (ord($this->data[$this->currentBytes++]) << 8) | ord($this->data[$this->currentBytes++]); 95 | $data['pps'][] = substr($this->data, $this->currentBytes, $len); 96 | $this->currentBytes += $len; 97 | } 98 | 99 | var_dump($data); 100 | return; 101 | */ 102 | 103 | //configurationVersion 104 | $this->skipBits(8); 105 | //profile 106 | $this->profile = $profile = $this->getBits(8); // read profile 107 | //profile compat 108 | $this->skipBits(8); 109 | $this->level = $level = $this->getBits(8); // level_idc 110 | 111 | $naluSize = ($this->getBits(8) & 0x03) + 1; 112 | $nb_sps = $this->getBits(8) & 0x1F; 113 | 114 | 115 | if ($nb_sps === 0) { 116 | echo "none sps", PHP_EOL; 117 | return; 118 | } 119 | 120 | //nalSize 121 | $this->getBits(16); 122 | 123 | /* nal type */ 124 | if (($this->getBits(8) & 0x1F) != 0x07) { 125 | return; 126 | } 127 | 128 | /* SPS */ 129 | $profile_idc = $this->getBits(8); 130 | 131 | /* flags */ 132 | $this->getBits(8); 133 | 134 | /* level idc */ 135 | $this->getBits(8); 136 | 137 | 138 | $this->expGolombUe(); // seq_parameter_set_id // sps 139 | 140 | if ($profile_idc == 100 || $profile_idc == 110 || 141 | $profile_idc == 122 || $profile_idc == 244 || $profile_idc == 44 || 142 | $profile_idc == 83 || $profile_idc == 86 || $profile_idc == 118) { 143 | /* chroma format idc */ 144 | $cf_idc = $this->expGolombUe(); 145 | 146 | if ($cf_idc == 3) { 147 | 148 | /* separate color plane */ 149 | $this->getBits(1); 150 | } 151 | 152 | /* bit depth luma - 8 */ 153 | $this->expGolombUe(); 154 | 155 | /* bit depth chroma - 8 */ 156 | $this->expGolombUe(); 157 | 158 | /* qpprime y zero transform bypass */ 159 | $this->getBits(1); 160 | 161 | /* seq scaling matrix present */ 162 | if ($this->getBits(1)) { 163 | 164 | for ($n = 0; $n < ($cf_idc != 3 ? 8 : 12); $n++) { 165 | 166 | /* seq scaling list present */ 167 | if ($this->getBits(1)) { 168 | 169 | /* TODO: scaling_list() 170 | if (n < 6) { 171 | } else { 172 | } 173 | */ 174 | } 175 | } 176 | } 177 | } 178 | 179 | /* log2 max frame num */ 180 | $this->expGolombUe(); 181 | 182 | /* pic order cnt type */ 183 | switch ($this->expGolombUe()) { 184 | case 0: 185 | 186 | /* max pic order cnt */ 187 | $this->expGolombUe(); 188 | break; 189 | 190 | case 1: 191 | 192 | /* delta pic order alwys zero */ 193 | $this->getBits(1); 194 | 195 | /* offset for non-ref pic */ 196 | $this->expGolombUe(); 197 | 198 | /* offset for top to bottom field */ 199 | $this->expGolombUe(); 200 | 201 | /* num ref frames in pic order */ 202 | $num_ref_frames = $this->expGolombUe(); 203 | 204 | for ($n = 0; $n < $num_ref_frames; $n++) { 205 | 206 | /* offset for ref frame */ 207 | $this->expGolombUe(); 208 | } 209 | } 210 | 211 | 212 | /* num ref frames */ 213 | $this->avc_ref_frames = $this->expGolombUe(); 214 | 215 | /* gaps in frame num allowed */ 216 | $this->getBits(1); 217 | 218 | /* pic width in mbs - 1 */ 219 | $width = $this->expGolombUe(); 220 | 221 | /* pic height in map units - 1 */ 222 | $height = $this->expGolombUe(); 223 | 224 | /* frame mbs only flag */ 225 | $frame_mbs_only = $this->getBits(1); 226 | 227 | if (!$frame_mbs_only) { 228 | 229 | /* mbs adaprive frame field */ 230 | $this->getBits(1); 231 | } 232 | 233 | /* direct 8x8 inference flag */ 234 | $this->getBits(1); 235 | 236 | /* frame cropping */ 237 | if ($this->getBits(1)) { 238 | 239 | $crop_left = $this->expGolombUe(); 240 | $crop_right = $this->expGolombUe(); 241 | $crop_top = $this->expGolombUe(); 242 | $crop_bottom = $this->expGolombUe(); 243 | 244 | } else { 245 | $crop_left = 0; 246 | $crop_right = 0; 247 | $crop_top = 0; 248 | $crop_bottom = 0; 249 | } 250 | 251 | $this->level = $this->level / 10.0; 252 | $this->width = ($width + 1) * 16 - ($crop_left + $crop_right) * 2; 253 | $this->height = (2 - $frame_mbs_only) * ($height + 1) * 16 - ($crop_top + $crop_bottom) * 2; 254 | 255 | } 256 | 257 | 258 | } -------------------------------------------------------------------------------- /packages/SabreAMF/CallbackServer.php: -------------------------------------------------------------------------------- 1 | operation) { 54 | 55 | case SabreAMF_AMF3_CommandMessage::CLIENT_PING_OPERATION : 56 | $response = new SabreAMF_AMF3_AcknowledgeMessage($request); 57 | break; 58 | case SabreAMF_AMF3_CommandMessage::LOGIN_OPERATION : 59 | $authData = base64_decode($request->body); 60 | if ($authData) { 61 | $authData = explode(':',$authData,2); 62 | if (count($authData)==2) { 63 | $this->authenticate($authData[0],$authData[1]); 64 | } 65 | } 66 | $response = new SabreAMF_AMF3_AcknowledgeMessage($request); 67 | $response->body = true; 68 | break; 69 | case SabreAMF_AMF3_CommandMessage::DISCONNECT_OPERATION : 70 | $response = new SabreAMF_AMF3_AcknowledgeMessage($request); 71 | break; 72 | default : 73 | throw new Exception('Unsupported CommandMessage operation: ' . $request->operation); 74 | 75 | } 76 | return $response; 77 | 78 | } 79 | 80 | /** 81 | * authenticate 82 | * 83 | * @param string $username 84 | * @param string $password 85 | * @return void 86 | */ 87 | protected function authenticate($username,$password) { 88 | 89 | if (is_callable($this->onAuthenticate)) { 90 | call_user_func($this->onAuthenticate,$username,$password); 91 | } 92 | 93 | } 94 | 95 | /** 96 | * invokeService 97 | * 98 | * @param string $service 99 | * @param string $method 100 | * @param array $data 101 | * @return mixed 102 | */ 103 | protected function invokeService($service,$method,$data) { 104 | 105 | if (is_callable($this->onInvokeService)) { 106 | return call_user_func_array($this->onInvokeService,array($service,$method,$data)); 107 | } else { 108 | throw new Exception('onInvokeService is not defined or not callable'); 109 | } 110 | 111 | } 112 | 113 | 114 | /** 115 | * exec 116 | * 117 | * @return void 118 | */ 119 | public function exec() { 120 | 121 | // First we'll be looping through the headers to see if there's anything we reconize 122 | 123 | foreach($this->getRequestHeaders() as $header) { 124 | 125 | switch($header['name']) { 126 | 127 | // We found a credentials headers, calling the authenticate method 128 | case 'Credentials' : 129 | $this->authenticate($header['data']['userid'],$header['data']['password']); 130 | break; 131 | 132 | } 133 | 134 | } 135 | 136 | foreach($this->getRequests() as $request) { 137 | 138 | // Default AMFVersion 139 | $AMFVersion = 0; 140 | 141 | $response = null; 142 | 143 | try { 144 | 145 | if (is_array($request['data']) && isset($request['data'][0]) && $request['data'][0] instanceof SabreAMF_AMF3_AbstractMessage) { 146 | $request['data'] = $request['data'][0]; 147 | } 148 | 149 | // See if we are dealing with the AMF3 messaging system 150 | if (is_object($request['data']) && $request['data'] instanceof SabreAMF_AMF3_AbstractMessage) { 151 | 152 | $AMFVersion = 3; 153 | 154 | // See if we are dealing with a CommandMessage 155 | if ($request['data'] instanceof SabreAMF_AMF3_CommandMessage) { 156 | 157 | // Handle the command message 158 | $response = $this->handleCommandMessage($request['data']); 159 | } 160 | 161 | // Is this maybe a RemotingMessage ? 162 | if ($request['data'] instanceof SabreAMF_AMF3_RemotingMessage) { 163 | 164 | // Yes 165 | $response = new SabreAMF_AMF3_AcknowledgeMessage($request['data']); 166 | $response->body = $this->invokeService($request['data']->source,$request['data']->operation,$request['data']->body); 167 | 168 | } 169 | 170 | } else { 171 | 172 | // We are dealing with AMF0 173 | $service = substr($request['target'],0,strrpos($request['target'],'.')); 174 | $method = substr(strrchr($request['target'],'.'),1); 175 | 176 | $response = $this->invokeService($service,$method,$request['data']); 177 | 178 | } 179 | 180 | $status = SabreAMF_Const::R_RESULT; 181 | 182 | } catch (Exception $e) { 183 | 184 | // We got an exception somewhere, ignore anything that has happened and send back 185 | // exception information 186 | 187 | if ($e instanceof SabreAMF_DetailException) { 188 | $detail = $e->getDetail(); 189 | } else { 190 | $detail = ''; 191 | } 192 | 193 | switch($AMFVersion) { 194 | case SabreAMF_Const::AMF0 : 195 | $response = array( 196 | 'description' => $e->getMessage(), 197 | 'detail' => $detail, 198 | 'line' => $e->getLine(), 199 | 'code' => $e->getCode()?$e->getCode():get_class($e), 200 | ); 201 | break; 202 | case SabreAMF_Const::AMF3 : 203 | $response = new SabreAMF_AMF3_ErrorMessage($request['data']); 204 | $response->faultString = $e->getMessage(); 205 | $response->faultCode = $e->getCode(); 206 | $response->faultDetail = $detail; 207 | break; 208 | 209 | } 210 | $status = SabreAMF_Const::R_STATUS; 211 | } 212 | 213 | $this->setResponse($request['response'],$status,$response); 214 | 215 | } 216 | $this->sendResponse(); 217 | 218 | } 219 | 220 | } 221 | 222 | 223 | --------------------------------------------------------------------------------