├── .gitignore ├── EasySwooleEvent.php ├── README.md ├── app ├── Command │ └── InstallCommand.php ├── Controller │ ├── Api.php │ ├── BC.php │ ├── Router.php │ └── Srs.php ├── Process │ ├── FFmpegProcess.php │ └── SrsProcess.php └── Util │ ├── Stream.php │ └── helpers.php ├── bootstrap.php ├── composer.json ├── config └── srs.php ├── dev.php ├── easyswoole ├── produce.php └── storage └── setup └── srs2.0.zip /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | srs2.0升级ffmpeg4.2.zip 3 | .idea/ 4 | vendor/ 5 | -------------------------------------------------------------------------------- /EasySwooleEvent.php: -------------------------------------------------------------------------------- 1 | set(SysConst::HTTP_CONTROLLER_NAMESPACE,'App\\Controller\\');//配置控制器命名空间 21 | $instance = \EasySwoole\EasySwoole\Config::getInstance(); 22 | defined('TEMP_PATH') or define('TEMP_PATH',$instance->getConf('TEMP_DIR')); 23 | defined('LOG_PATH') or define('LOG_PATH',$instance->getConf('LOG_DIR')); 24 | 25 | defined('SRS_ERROR') or define('SRS_ERROR',1); 26 | defined('SRS_SUCCESS') or define('SRS_SUCCESS',0); 27 | 28 | date_default_timezone_set('Asia/Shanghai'); 29 | } 30 | 31 | public static function mainServerCreate(EventRegister $register) 32 | { 33 | self::loadConfig(); 34 | self::initSwooleTable(); 35 | self::initProcess(); 36 | self::initTimer(); 37 | } 38 | 39 | /** 40 | * 加载自定义配置文件 41 | */ 42 | public static function loadConfig() 43 | { 44 | $instance = \EasySwoole\EasySwoole\Config::getInstance(); 45 | foreach (glob(EASYSWOOLE_ROOT.DIRECTORY_SEPARATOR.'config/*.php') as $filePath){ 46 | $instance->setConf(rtrim(basename($filePath),'.php'),require_once $filePath); 47 | } 48 | } 49 | 50 | /** 51 | * 加载内存表 52 | */ 53 | public static function initSwooleTable() 54 | { 55 | //记录流对应进程号 key=stream_key 56 | TableManager::getInstance()->add('stream', ['php_pid'=>['type'=>Table::TYPE_INT,'size'=>11],], 1024); 57 | //记录流下客户端(实现自动结束流) key=stream_key 58 | TableManager::getInstance()->add('watch', ['rows'=>['type'=>Table::TYPE_STRING,'size'=>4096]], 1024); 59 | //记录客户端对应流(实现自动结束流) key=client_id 60 | TableManager::getInstance()->add('client', ['stream_key'=>['type'=>Table::TYPE_STRING,'size'=>32]], 1024); 61 | } 62 | 63 | /** 64 | * 初始化自定义进程 65 | */ 66 | public static function initProcess() 67 | { 68 | dump(getmypid()); 69 | /**srs进程**/ 70 | ServerManager::getInstance()->addProcess(new SrsProcess(),'srs'); 71 | /**ffmpeg进程**/ 72 | $processConfig = new \EasySwoole\Component\Process\Config(); 73 | $processConfig->setProcessName('ffmpeg'); 74 | $processConfig->setPipeType(SOCK_STREAM);//DGRAM出现丢包,问题仅存在于internet网络的UDP通信 75 | $processConfig->setEnableCoroutine(true); 76 | ServerManager::getInstance()->addProcess(new FFmpegProcess($processConfig),'ffmpeg'); 77 | } 78 | 79 | 80 | /** 81 | * 初始化定时器 82 | */ 83 | public static function initTimer() 84 | { 85 | 86 | } 87 | 88 | public static function onRequest(Request $request, Response $response): bool 89 | { 90 | return true; 91 | } 92 | 93 | public static function afterRequest(Request $request, Response $response): void 94 | { 95 | // TODO: Implement afterAction() method. 96 | } 97 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## PHP跨平台直播 2 | 3 | 支持: 4 | * 按需拉流,RTSP转RTMP,HLS 5 | * 客户端主动推流,封装多码率 6 | * 同步录制 7 | * 多级集群 8 | 9 | 更多支持请参阅 10 | https://github.com/ossrs/srs/wiki/v2_CN_Home 11 | 12 | ###一.基本信息: 13 | 14 | 15 | 1.入门推荐书籍: 16 | * [FFmpeg从入门到精通](https://book.douban.com/subject/30178432/) 17 | * [Swoole从入门到精通](https://wiki.swoole.com/wiki/page/1.html) 18 | * [SRS概述 必读!!!!!!!!!!!!!!](https://github.com/ossrs/srs/wiki/v2_CN_Home) 19 | 20 | 2.环境 21 | 22 | php => 7.1.0 23 | swoole => 4.4 24 | easyswoole => 3.3 25 | 26 | 27 | 3.使用: 28 | 29 | 1. composer install 30 | 2. php easyswoole install_srs 31 | 3. php easwswoole start 32 | 33 | ###一.HTTP API接口 34 | 35 | *** 36 | 1.添加设备 37 | 38 | POST http://服务器ip:服务端口/api/create 39 | { 40 | "stream": "唯一名称", 41 | "live_host": "rtsp://账号:密码@摄像头ip:摄像头端口/stream1",//不同设备rtsp地址可能不一样 42 | "app": "live" 43 | } 44 | 45 | 2.编辑设备 46 | 47 | POST http://服务器ip:服务端口/api/update 48 | { 49 | "stream": "唯一名称", 50 | "live_host": "rtsp://账号:密码@摄像头ip:摄像头端口/stream1",//不同设备rtsp地址可能不一样 51 | "app": "live" 52 | } 53 | 54 | 2.删除设备 55 | 56 | POST http://服务器ip:服务端口/api/destroy 57 | { 58 | "stream": "唯一名称", 59 | "app": "live" 60 | } 61 | 62 | 63 | ###二.观看 64 | 65 | *** 66 | rtmp://服务器ip:1935/{app}/{stream} -------------------------------------------------------------------------------- /app/Command/InstallCommand.php: -------------------------------------------------------------------------------- 1 | ['pipe','r'], 1=>["pipe", "w"], 2=>["pipe", "w"]], $pipes, '/bin/bash'); 28 | if(is_resource($process)){ 29 | while($ret=fgets($pipes[1])){echo ''.$ret;} 30 | } 31 | fclose($pipes[0]); 32 | fclose($pipes[1]); 33 | fclose($pipes[2]); 34 | proc_close($process); 35 | //中途会退出 再次执行 36 | exec('cd '.dirname($setupZip).'/srs && make --jobs='.swoole_cpu_num(),$out); 37 | echo implode($out,"\n"); 38 | return null; 39 | } 40 | 41 | public function help(array $args): ?string 42 | { 43 | return "安装SRS流媒体服务器"; 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /app/Controller/Api.php: -------------------------------------------------------------------------------- 1 | all(); 20 | if(empty($data['stream'])||empty($data['live_host'])||empty($data['app'])||strlen($data['app'])>10){throw new \Exception('参数错误');} 21 | //判断是否存在 22 | if (Stream::exists(stream_key($data['app'],$data['stream']))) {throw new \Exception('该资源已存在');} 23 | Stream::set($data['stream'],$data['live_host'],$data['app']); 24 | $this->success(); 25 | }catch (\Exception $exception){ 26 | $this->error([],$exception->getMessage()); 27 | } 28 | } 29 | 30 | /** 31 | * 更新 32 | */ 33 | public function update() 34 | { 35 | try{ 36 | $data=$this->all(); 37 | if(empty($data['stream'])||empty($data['live_host'])||empty($data['app'])||strlen($data['app'])>10){throw new \Exception('参数错误');} 38 | //判断是否存在 39 | if (!Stream::exists(stream_key($data['app'],$data['stream']))) {throw new \Exception('该资源不存在');} 40 | Stream::set($data['stream'],$data['live_host'],$data['app']); 41 | $this->success(); 42 | }catch (\Exception $exception){ 43 | $this->error([],$exception->getMessage()); 44 | } 45 | } 46 | 47 | /** 48 | * 删除 49 | */ 50 | public function destroy() 51 | { 52 | try{ 53 | $data=$this->all(); 54 | if(empty($data['stream'])||empty($data['app'])){throw new \Exception('参数错误');} 55 | Stream::del(stream_key($data['app'],$data['stream'])); 56 | $this->success(); 57 | }catch (\Exception $exception){ 58 | $this->error([],$exception->getMessage()); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /app/Controller/BC.php: -------------------------------------------------------------------------------- 1 | response()->withStatus(404); 14 | $file = EASYSWOOLE_ROOT.'/vendor/easyswoole/easyswoole/src/Resource/Http/404.html'; 15 | if(!is_file($file)){ 16 | $file = EASYSWOOLE_ROOT.'/src/Resource/Http/404.html'; 17 | } 18 | $this->response()->write(file_get_contents($file)); 19 | } 20 | 21 | protected function actionNotFound(?string $action) 22 | { 23 | $this->response()->withStatus(404); 24 | $file = EASYSWOOLE_ROOT.'/vendor/easyswoole/easyswoole/src/Resource/Http/404.html'; 25 | if(!is_file($file)){ 26 | $file = EASYSWOOLE_ROOT.'/src/Resource/Http/404.html'; 27 | } 28 | $this->response()->write(file_get_contents($file)); 29 | } 30 | 31 | /**获取一个参数 32 | * @param string $key 33 | * @return array|mixed 34 | */ 35 | protected function input(string $key) 36 | { 37 | return $this->request()->getRequestParam($key); 38 | } 39 | 40 | /**获取所有参数 41 | * @return array 42 | */ 43 | protected function all() 44 | { 45 | return $this->request()->getRequestParam(); 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | protected function raw(){ 52 | return $this->request()->getBody()->__toString(); 53 | } 54 | 55 | /**成功 56 | * @param array $data 57 | * @param string $msg 58 | */ 59 | protected function success(array $data=[],$msg='成功'){ 60 | $this->response()->write(json_encode(['code'=>1,'msg'=>$msg,'data'=>$data])); 61 | $this->response()->end(); 62 | } 63 | 64 | /**失败 65 | * @param array $data 66 | * @param string $msg 67 | */ 68 | protected function error(array $data=[],$msg='失败'){ 69 | $this->response()->write(json_encode(['code'=>0,'msg'=>$msg,'data'=>$data])); 70 | $this->response()->end(); 71 | } 72 | } -------------------------------------------------------------------------------- /app/Controller/Router.php: -------------------------------------------------------------------------------- 1 | addApi($routeCollector); 17 | $this->addSrs($routeCollector); 18 | $this->setGlobalMode(true); 19 | $this->setMethodNotAllowCallBack(function (Request $request,Response $response){ 20 | $response->withStatus(403); 21 | return false;//结束此次响应 22 | }); 23 | $this->setRouterNotFoundCallBack(function (Request $request,Response $response){ 24 | $response->withStatus(404); 25 | return false;//结束此次响应 26 | }); 27 | } 28 | 29 | private function addSrs(RouteCollector $routeCollector) 30 | { 31 | $routeCollector->addGroup('/srs',function (RouteCollector $routeCollector){ 32 | $routeCollector->addRoute('POST', '/onConnect','/Srs/onConnect'); 33 | $routeCollector->addRoute('POST', '/onClose','/Srs/onClose'); 34 | $routeCollector->addRoute('POST', '/onPublish','/Srs/onPublish'); 35 | $routeCollector->addRoute('POST', '/onUnpublish','/Srs/onUnpublish'); 36 | $routeCollector->addRoute('POST', '/onPlay','/Srs/onPlay'); 37 | $routeCollector->addRoute('POST', '/onStop','/Srs/onStop'); 38 | $routeCollector->addRoute('POST', '/onDvr','/Srs/onDvr'); 39 | $routeCollector->addRoute('POST', '/onHls','/Srs/onHls'); 40 | $routeCollector->addRoute('POST', '/onHlsNotify','/Srs/onHlsNotify'); 41 | $routeCollector->addRoute('POST', '/heartbeat','/Srs/heartbeat'); 42 | }); 43 | } 44 | 45 | private function addApi(RouteCollector$routeCollector) 46 | { 47 | $routeCollector->addGroup('/api',function (RouteCollector $routeCollector){ 48 | $routeCollector->addRoute('POST', '/create','/Api/create'); 49 | $routeCollector->addRoute('POST', '/update','/Api/update'); 50 | $routeCollector->addRoute('POST', '/destroy','/Api/destroy'); 51 | }); 52 | } 53 | } -------------------------------------------------------------------------------- /app/Controller/Srs.php: -------------------------------------------------------------------------------- 1 | retSrs(); 22 | } 23 | 24 | /** 25 | * 连接 26 | */ 27 | public function onConnect() 28 | { 29 | try{ 30 | dump(__FUNCTION__); 31 | // $data=json_decode($this->raw(),true); 32 | $this->retSrs(); 33 | }catch (\Exception $exception){ 34 | $this->retSrs(SRS_ERROR); 35 | } 36 | } 37 | 38 | /** 39 | * 关闭连接 不仅仅是播放端,还有推流端 40 | * {"action": "on_close", "client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","send_bytes": 10240, "recv_bytes": 10240} 41 | */ 42 | public function onClose() 43 | { 44 | try{ 45 | dump(__FUNCTION__); 46 | $data=json_decode($this->raw(),true); 47 | if(empty($data)){throw new \Exception();} 48 | $watchTable=TableManager::getInstance()->get('watch'); 49 | $clientTable=TableManager::getInstance()->get('client'); 50 | //判断是否是播放端 51 | if (!$clientTable->exist($data['client_id'])) {throw new \Exception();} 52 | 53 | //获取客户端对应流 54 | $clientStreamKey=$clientTable->get($data['client_id'],'stream_key'); 55 | //判断流下客户端 56 | $watchClient=$watchTable->get($clientStreamKey,'rows'); 57 | $watchClient=$watchClient?json_decode($watchClient,true):[]; 58 | //删除客户端对应流 59 | $clientTable->del($data['client_id']); 60 | 61 | //删除流下客户端(当前连接) 62 | foreach ($watchClient as $key=>$client_id){if ($client_id==$data['client_id']) {unset($watchClient[$key]);}} 63 | 64 | if(empty($watchClient)){ 65 | //结束进程 66 | Stream::stop($clientStreamKey); 67 | }else{ 68 | $watchTable->set($clientStreamKey,['rows'=>json_encode($watchClient)]); 69 | } 70 | $this->retSrs(); 71 | }catch (\Exception $exception){ 72 | $this->retSrs();//不能阻止关闭 73 | } 74 | } 75 | 76 | /** 77 | * 发布流 78 | * {"action": "on_publish","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","stream": "livestream" } 79 | */ 80 | public function onPublish() 81 | { 82 | try{ 83 | dump(__FUNCTION__); 84 | $data=json_decode($this->raw(),true); 85 | if(empty($data)){throw new \Exception();} 86 | // $streamTable=TableManager::getInstance()->get('stream'); 87 | // $streamTable->set(stream_key($data['app'],$data['stream']),['php_pid'=>$phpProcessPid]); 88 | $this->retSrs(); 89 | }catch (\Exception $exception){ 90 | $this->retSrs(SRS_ERROR); 91 | } 92 | } 93 | 94 | /** 95 | * 停止发布流 96 | * {"action": "on_unpublish","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","stream": "livestream"} 97 | */ 98 | public function onUnpublish() 99 | { 100 | try{ 101 | dump(__FUNCTION__); 102 | $data=json_decode($this->raw(),true); 103 | if(empty($data)||!Stream::exists(stream_key($data['app'],$data['stream']))){throw new \Exception();} 104 | Stream::stop(stream_key($data['app'],$data['stream'])); 105 | $this->retSrs(); 106 | }catch (\Exception $exception){ 107 | $this->retSrs(SRS_ERROR); 108 | } 109 | } 110 | 111 | /** 112 | * 开始播放 113 | *{"action": "on_play","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","stream": "livestream","pageUrl": "http://www.test.com/live.html"} 114 | */ 115 | public function onPlay() 116 | { 117 | try{ 118 | dump(__FUNCTION__); 119 | $data=json_decode($this->raw(),true); 120 | if(empty($data)||!Stream::exists(stream_key($data['app'],$data['stream']))){throw new \Exception();} 121 | //发送到 自定义ffmpeg进程 122 | if (ServerManager::getInstance()->getProcess('ffmpeg')->write(stream_key($data['app'],$data['stream']))===false) {throw new \Exception();} 123 | 124 | $clientTable=TableManager::getInstance()->get('client'); 125 | $watchTable=TableManager::getInstance()->get('watch'); 126 | 127 | //记录客户端对应流 128 | $clientTable->set($data['client_id'],['stream_key'=>stream_key($data['app'],$data['stream'])]); 129 | 130 | //记录流下客户端 131 | $watchClient=$watchTable->get(stream_key($data['app'],$data['stream']),'rows'); 132 | $watchClient=$watchClient?json_decode($watchClient,true):[]; 133 | array_push($watchClient,$data['client_id']); 134 | $watchTable->set(stream_key($data['app'],$data['stream']),['rows'=>json_encode($watchClient)]); 135 | 136 | $this->retSrs(); 137 | }catch (\Exception $exception){ 138 | $this->retSrs(SRS_ERROR); 139 | } 140 | } 141 | 142 | /** 143 | * 当客户端停止播放时。备注:停止播放可能不会关闭连接,还能再继续播放。 144 | */ 145 | public function onStop() 146 | { 147 | try{ 148 | dump(__FUNCTION__); 149 | // $data=json_decode($this->raw(),true); 150 | $this->retSrs(); 151 | }catch (\Exception $exception){ 152 | $this->retSrs(SRS_ERROR); 153 | } 154 | } 155 | 156 | public function onDvr() 157 | { 158 | try{ 159 | dump(__FUNCTION__); 160 | // $data=json_decode($this->raw(),true); 161 | $this->retSrs(); 162 | }catch (\Exception $exception){ 163 | $this->retSrs(SRS_ERROR); 164 | } 165 | } 166 | 167 | public function onHls() 168 | { 169 | try{ 170 | dump(__FUNCTION__); 171 | 172 | // $data=json_decode($this->raw(),true); 173 | $this->retSrs(); 174 | }catch (\Exception $exception){ 175 | $this->retSrs(SRS_ERROR); 176 | } 177 | } 178 | 179 | public function onHlsNotify() 180 | { 181 | try{ 182 | dump(__FUNCTION__); 183 | 184 | // $data=json_decode($this->raw(),true); 185 | $this->retSrs(); 186 | }catch (\Exception $exception){ 187 | $this->retSrs(SRS_ERROR); 188 | } 189 | } 190 | 191 | /**发送srs响应 192 | * @param int $code 193 | */ 194 | private function retSrs(int $code=SRS_SUCCESS) 195 | { 196 | $this->response()->write($code); 197 | $this->response()->withStatus(200); 198 | $this->response()->end(); 199 | } 200 | } -------------------------------------------------------------------------------- /app/Process/FFmpegProcess.php: -------------------------------------------------------------------------------- 1 | $instance->getConf('srs.srs_path').'/objs/ffmpeg/bin/ffmpeg', 45 | 'ffprobe.binaries' => $instance->getConf('srs.srs_path').'/objs/ffmpeg/bin/ffprobe', 46 | 'timeout'=>0, 47 | 'ffmpeg.threads' => swoole_cpu_num(), //FFMpeg应该使用的线程数 48 | ]); 49 | $video = $ffmpeg->open($row['rtsp_host']); 50 | $video_format=$video->getFormat()->all(); 51 | $video_info=$video->getStreams()->videos()->first()->all(); 52 | $audio_info=$video->getStreams()->audios()->first()->all(); 53 | 54 | $format=new X264(); 55 | $format 56 | ->setKiloBitrate(1024) //码率 比特率 57 | ->setAudioChannels($audio_info['channels']) // 声道设置,1单声道,2双声道,3立体声 58 | // ->setAudioKiloBitrate($audio_bit_rate)//音频比特率 59 | ->setAudioCodec('libfdk_aac') 60 | ->setAdditionalParameters(['-tune','zerolatency','-preset','ultrafast','-vf','scale=-2:480','-f','flv']);//'-an' 61 | //此代码阻塞 62 | $instance=\EasySwoole\EasySwoole\Config::getInstance(); 63 | $video->save($format, "rtmp://127.0.0.1:{$instance->getConf('srs.rtmp_port')}/".$row['app']."/".$row['stream_id']); 64 | },false,SOCK_STREAM,false); 65 | if (($phpProcessPid=$ffmpegProcess->start())===false){return false;} 66 | $streamTable=TableManager::getInstance()->get('stream'); 67 | $streamTable->set($stream_key,['php_pid'=>$phpProcessPid]); 68 | } 69 | 70 | 71 | protected function onPipeReadable(\Swoole\Process $process) 72 | { 73 | /* 74 | * 该回调可选 75 | * 当有主进程对子进程发送消息的时候,会触发的回调,触发后,务必使用 76 | * $process->read()来读取消息 77 | */ 78 | try{ 79 | $stream_key=$this->getProcess()->read(32); 80 | //判断是否存在 81 | if(!Stream::exists($stream_key)){return false;} 82 | //避免重复调用 83 | $streamTable=TableManager::getInstance()->get('stream'); 84 | if (!$streamTable->exist($stream_key)) { 85 | $this->pullStream($stream_key); 86 | } 87 | }catch (\Exception $exception){ 88 | dump($exception->getMessage()); 89 | } 90 | } 91 | 92 | protected function onShutDown() 93 | { 94 | /* 95 | * 该回调可选 96 | * 当该进程退出的时候,会执行该回调 97 | */ 98 | } 99 | 100 | protected function onException(\Throwable $throwable, ...$args) 101 | { 102 | /* 103 | * 该回调可选 104 | * 当该进程出现异常的时候,会执行该回调 105 | */ 106 | } 107 | } -------------------------------------------------------------------------------- /app/Process/SrsProcess.php: -------------------------------------------------------------------------------- 1 | getConf('srs'); 17 | 18 | try{ 19 | $this->loadSrsConfig($config['srs_path'],$config['srs_config']); 20 | $this->start($config['srs_path']); 21 | }catch (\Exception $exception){ 22 | dump($exception->getMessage()); 23 | } 24 | 25 | } 26 | 27 | 28 | /**加载配置文件 29 | * @param string $srsBasePath 30 | * @param array $srsConf 31 | * @return bool 32 | * @throws \Exception 33 | */ 34 | private function loadSrsConfig(string $srsBasePath,array $srsConf) 35 | { 36 | //检测二进制文件 37 | if (!file_exists($srsBasePath.'/objs/srs')) {throw new \Exception('没有找到srs可执行程序');} 38 | if(empty($srsConf)){echo "srs配置信息加载失败\n\n";return false;} 39 | $srsConf=json_encode($srsConf); 40 | $srsConf=substr($srsConf, 1); 41 | $srsConf=substr($srsConf, 0, -1); 42 | $srsConf=str_replace("\",\"","\n",$srsConf);//替换掉每个属性的逗号 43 | $srsConf=str_replace("\":\"",' ',$srsConf); 44 | $srsConf=str_replace("\"",'',$srsConf); 45 | $srsConf=str_replace("\\",'',$srsConf);//url等的http:\/ 46 | $srsConf=str_replace(",",'',$srsConf); 47 | $srsConf=str_replace(":{"," { \n",$srsConf);//键值对的 冒号 48 | $srsConf=str_replace(";}"," ;} \n",$srsConf);//键值对的 冒号 49 | $myConf = fopen($this->srsTempConfPath, "w"); 50 | fwrite($myConf,$srsConf); 51 | fclose($myConf); 52 | } 53 | 54 | 55 | /**启动服务 56 | * @param string $srsBasePath 57 | */ 58 | private function start(string $srsBasePath) 59 | { 60 | $this->getProcess()->exec($srsBasePath."/objs/srs", ['-c',$this->srsTempConfPath]); 61 | } 62 | 63 | protected function onPipeReadable(\Swoole\Process $process) 64 | { 65 | /* 66 | * 该回调可选 67 | * 当有主进程对子进程发送消息的时候,会触发的回调,触发后,务必使用 68 | * $process->read()来读取消息 69 | */ 70 | } 71 | 72 | protected function onShutDown() 73 | { 74 | /* 75 | * 该回调可选 76 | * 当该进程退出的时候,会执行该回调 77 | */ 78 | } 79 | 80 | protected function onException(\Throwable $throwable, ...$args) 81 | { 82 | /* 83 | * 该回调可选 84 | * 当该进程出现异常的时候,会执行该回调 85 | */ 86 | } 87 | } -------------------------------------------------------------------------------- /app/Util/Stream.php: -------------------------------------------------------------------------------- 1 | $stream_id, 36 | 'rtsp_host'=>$rtspHost, 37 | 'app'=>$app, 38 | ])); 39 | fclose($f); 40 | } 41 | 42 | /**删除 43 | * @param string $stream_key 44 | * @return bool 45 | */ 46 | public static function del(string $stream_key) 47 | { 48 | if (self::exists($stream_key)) { 49 | return unlink(self::$path.$stream_key); 50 | } 51 | return false; 52 | } 53 | 54 | 55 | /**判断是否存在 56 | * @param string $stream_key 57 | * @return bool 58 | */ 59 | public static function exists(string $stream_key) 60 | { 61 | return file_exists(self::$path.$stream_key); 62 | } 63 | 64 | 65 | /**结束指定流 66 | * @param string $stream_key 67 | */ 68 | public static function stop(string $stream_key) 69 | { 70 | $streamTable=TableManager::getInstance()->get('stream'); 71 | $watchTable=TableManager::getInstance()->get('watch'); 72 | if (!$streamTable->exist($stream_key)) {return;} 73 | $php_pid=$streamTable->get($stream_key,'php_pid'); 74 | //检测php进程 75 | if (\swoole_process::kill($php_pid, 0)) { 76 | //根据父进程号结束所有子进程 77 | exec("pkill -P {$php_pid}"); 78 | //清空用户(还需要踢掉用户) 79 | $watchTable->del($stream_key); 80 | } 81 | $streamTable->del($stream_key); 82 | } 83 | } -------------------------------------------------------------------------------- /app/Util/helpers.php: -------------------------------------------------------------------------------- 1 | open($filepath); 20 | if ($res === TRUE) { 21 | //解压缩到$extractTo指定的文件夹 22 | $zip->extractTo($extractTo); 23 | $zip->close(); 24 | return true; 25 | } else { 26 | echo 'failed, code:' . $res; 27 | return false; 28 | } 29 | } -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | set(new \App\Command\InstallCommand()); 5 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "easyswoole/easyswoole": "^3.3", 4 | "php-ffmpeg/php-ffmpeg": "^0.14.0", 5 | "symfony/var-dumper": "^5.0" 6 | }, 7 | "autoload": { 8 | "psr-4": { 9 | "App\\": "app/" 10 | }, 11 | "files": [ 12 | "app/Util/helpers.php" 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /config/srs.php: -------------------------------------------------------------------------------- 1 | EASYSWOOLE_ROOT.'/storage/setup/srs', 8 | 'rtmp_port'=>$rtmp_port, 9 | //srs 配置文件 10 | 'srs_config'=>[ 11 | 'listen'=>$rtmp_port.';',//监听端口 rtmp 12 | 'max_connections'=>'1000;',//最大连接数 13 | 'daemon'=>'off;',//后台启动 on|off 14 | 'pid'=>TEMP_PATH.'/srs.pid'.';',//pid文件路径 15 | 'ff_log_dir'=>TEMP_PATH.'/srs'.';',//日志目录。如果启用ffmpeg,每个转码流将创建一个日志文件。 /dev/null禁用日志。默认./objs。 16 | 'srs_log_tank'=>'console;',//日志文件打印位置 console|file 17 | 'srs_log_level'=>'error;',//日志级别 从高到低 verbose|info|trace|warn|error 18 | 'srs_log_file'=>LOG_PATH.'/srs.log'.';',//日志文件位置 19 | 'heartbeat'=>[//心跳 20 | 'enabled'=>'off;',// on|off 21 | 'interval'=>'3;',//心跳的间隔秒 0.3的倍数 22 | 'url'=>$host.'/heartbeat;',//必须是一个restful的HTTP API URL, 数据:{"device_id": "my-srs-device","ip": "192.168.1.100"} 23 | 'device_id'=>'master;',//这个设备的id 24 | 'summaries'=>'off;'//是否有摘要报告 数据:{"summaries": summaries object.} 25 | ], 26 | 'http_api'=>[//是否启用HTTP API 27 | 'enabled'=>'off;',// on|off 28 | 'listen'=>'1936;',//监听端口 29 | 'crossdomain'=>'off;',//跨域请求 on|off 30 | ], 31 | 'http_server'=>[//是否启用HTTP Server 配合http_remux使用 32 | 'enabled'=>'off;',// on|off 33 | 'listen'=>'8080;',//监听端口 34 | 'dir'=>EASYSWOOLE_ROOT.'/public;', 35 | ], 36 | //从其他协议到SRS的RTMP流。 37 | 'stream_caster'=>[ 38 | 'enabled'=>'off;',// on|off 39 | //流类型 40 | # mpegts_over_udp, MPEG-TS over UDP caster. 41 | # rtsp, Real Time Streaming Protocol (RTSP). 42 | # flv, FLV over HTTP POST. 43 | 'caster'=>'rtsp;', 44 | 'output'=>'rtmp://127.0.0.1/[app]/[stream];', 45 | /** 46 | * 对于MPEGTSUPROUDPCAST,请在UDP端口监听。例如,8935。 47 | 对于RTSP连铸机,在TCP端口监听。例如,554。 48 | 对于FLV连铸机,在TCP端口监听。例如,8936。 49 | 支持:监听<[IP:]端口> 50 | */ 51 | 'listen'=>'554;', 52 | /** for the rtsp caster, the rtp server local port over udp, 53 | which reply the rtsp setup request message, the port will be used: 54 | */ 55 | 'rtp_port_min'=>'57200;', 56 | 'rtp_port_max'=>'57300;', 57 | ], 58 | 'vhost __defaultVhost__'=>[ 59 | #'forward'=>'192.168.1.6:1935 192.168.1.7:1935;',//热备 转发到其他源站 (主) master 60 | #'mode'=>'remote',//边缘配置 origin为master 的地址 61 | #'origin'=>'192.168.1.81:1935',// 62 | //============================考虑GOP-Cache和累积延迟,推荐的低延时配置======================== 63 | 'gop_cache'=>'off;',//打开: 始终保留一个关键帧,客户端立即播放(延迟增大) 关闭:等待关键帧到来(等待期间黑屏) 如果需要最小延迟,则设置为off;如果需要客户快速启动,则设置为on。 64 | 'queue_length'=>'8;',//累积延迟 配置直播队列的长度,服务器会将数据放在直播队列中,如果超过这个长度就清空到最后一个I帧:当然这个不能配置太小,譬如GOP是1秒,queue_length是1秒,这样会导致有1秒数据就清空,会导致跳跃。 65 | 'min_latency'=>'on;',//最小延迟 66 | /* 67 | *是否启用MR(merged -read) 开启后 性能+,延迟+,和内存+, 68 | * 例如,延迟= 500ms,kbps = 3000kbps,每个发布连接都会消耗 69 | * 内存= 500 * 3000 / 8 = 187500B = 183KB 70 | * 当有2500个出版商时,SRS的总内存至少是: 183KB * 2500 = 446MB 71 | * 推荐300-2000;默认值:350 若需要低延迟配置,关闭merged-read,服务器每次收到1个包就会解析 72 | */ 73 | 'mr'=>['enabled'=>'off;'], 74 | /* 75 | * erged-Write,即一次发送N毫秒的包给客户端。这个算法可以将RTMP下行的效率提升5倍左右,SRS1.0每次writev一个packet支持2700客户端,SRS2.0一次writev多个packet支持10000客户端。 76 | * 用户可以配置merged-write一次写入的包的数目,建议不做修改: 77 | * 推荐300-1800;默认值350 78 | */ 79 | 'mw_latency'=>'100;', 80 | 'tcp_nodelay'=>'on;', 81 | 'http_hooks'=>[//http回调post请求 服务器必须返回HTTP代码200(Stauts OK) 和响应头 错误码是int型 0代表成功 82 | 'enabled'=>'on;', 83 | 'on_connect'=>$host.'/onConnect;',//客户端连接到指定的vhost和app时 {"action": "on_connect","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","tcUrl": "rtmp://video.test.com/live?key=d2fa801d08e3f90ed1e1670e6e52651a", "pageUrl": "http://www.test.com/live.html"} 84 | 'on_close'=>$host.'/onClose;',//关闭连接,或者SRS主动关闭连接 {"action": "on_close", "client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","send_bytes": 10240, "recv_bytes": 10240} 85 | 'on_publish'=>$host.'/onPublish;',//当客户端发布流时 {"action": "on_publish","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","stream": "livestream" } 86 | 'on_unpublish'=>$host.'/onUnpublish;',//当客户端停止发布流时{"action": "on_unpublish","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","stream": "livestream"} 87 | 'on_play'=>$host.'/onPlay;',//当客户端开始播放流时{"action": "on_play","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","stream": "livestream","pageUrl": "http://www.test.com/live.html"} 88 | 'on_stop'=>$host.'/onStop;',//当客户端停止播放时 停止播放可能不会关闭连接,还能再继续播放。{"action": "on_stop","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","stream": "livestream"} 89 | 'on_dvr'=>$host.'/onDvr;',//当切片生成时{"action": "on_dvr","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","stream": "livestream","cwd": "/usr/local/srs","file": "./objs/nginx/html/live/livestream.1420254068776.flv"} 90 | 'on_hls'=>$host.'/onHls;',//{"action": "on_hls","client_id": 1985,"ip": "192.168.1.10", "vhost": "video.test.com", "app": "live","stream": "livestream","duration": 9.36, // in seconds"cwd": "/usr/local/srs","file": "./objs/nginx/html/live/livestream/2015-04-23/01/476584165.ts","url": "live/livestream/2015-04-23/01/476584165.ts","m3u8": "./objs/nginx/html/live/livestream/live.m3u8","m3u8_url": "live/livestream/live.m3u8","seq_no": 100} 91 | 'on_hls_notify'=>$host.'/onHlsNotify;',//当切片生成时,回调这个url,使用GET回调 92 | ], 93 | 'http_remux'=>[//HTTP FLV 94 | 'enabled'=>'off;',//是否启用 95 | 'fast_cache'=>'0;',//音频流(mp3/aac)的快速缓存,0为禁用 96 | 'mount' =>'[vhost]/[app]/[stream].flv;',// http的端口由http_server部分指定 97 | 'hstrs' =>'on;', 98 | ], 99 | 'dvr'=>[//srs将流 录制 成flv文件 100 | 'enabled'=>'off;', 101 | 'dvr_path'=>EASYSWOOLE_ROOT.'/public/[app]/[stream].[timestamp].flv;', 102 | 'dvr_plan'=>'session;', 103 | 'dvr_duration'=>'30;', 104 | 'dvr_wait_keyframe'=>'on;', 105 | 'time_jitter'=>'full;', 106 | ], 107 | 'hls'=>[// 108 | 'enabled'=>'off;',// on|off 109 | /*单位秒,指定ts切片的最小长度。默认为10 110 | * ts文件长度 = max(hls_fragment, gop_size) 如果ffmpeg中指定fps(帧速率)为20帧/秒,gop为200帧,那么gop_size=gop/fps=10秒 111 | * 那么实际ts的长度为max(5,10) =10秒。这样实际ts切片的长度就与设定的不同了。 112 | */ 113 | 'hls_fragment'=>'3;', 114 | /*倍数,控制m3u8的EXT-X-TARGETDURATION值,EXT-X-TARGETDURATION(整数)值标明了切片的最大时长。 115 | * m3u8列表文件中EXTINF的值必须小于等于EXT-X-TARGETDURATION的值。 116 | * EXT-X-TARGETDURATION在m3u8列表文件中必须出现一次。 117 | */ 118 | 'hls_td_ratio'=>'1.5;', 119 | 'hls_aof_ratio'=>'2.0;',//倍数。纯音频时,当ts时长超过配置的ls_fragment乘以这个系数时就切割文件。例如,当ls_fragment是10秒,hls_aof_ratio是2.0时,对于纯音频,10s*2.0=20秒时就切割ts文件。 120 | 'hls_window'=>'10;',//单位:秒,指定HLS窗口大小,即m3u8中ts文件的时长之和,超过总时长后,丢弃第一个m3u8中的第一个切片,直到ts的总时长在这个配置项范围之内。即SRS保证:hls_window的值必须大于等于m3u8列表文件中所有ts切片时长的总和。 121 | 'hls_on_error'=>'ignore;',// 错误策略 ignore:当错误发生时,忽略错误并停止输出hls(默认) disconnect:当发生错误时,断开推流连接 continue:当发生错误时,忽略错误并继续输出hls 122 | 'hls_storage'=>'disk;',//存储方式 disk:把m3u8/ts写到磁盘 发送m3u8/ts到内存,但是必须使用srs自带的http server进行分发。 both, disk and ram。 123 | 'hls_path'=>EASYSWOOLE_ROOT.'/public/hls'.';',//当hls写到磁盘时,指定写入的目录。 124 | 'hls_m3u8_file'=>'[app]/[stream]/[stream].m3u8;',//生成hls的m3u8文件的文件名,有一些变量可用于生成m3u8文件的文件名: 125 | 'hls_ts_file'=>'[app]/[stream]/[stream]-[seq].ts;',//m3u8文件的绝对路径为[SRS_Path]/objs/nginx/html/[app]/[stream].m3u8 126 | 'hls_ts_floor'=>'off;',//是否使用floor的方式生成hls ts文件的路径。如实hls_ts_floor on; 使用timestamp/hls_fragment作为[timestamp]变量,即[timestamp]=timestamp/hls_fragment,并且使用enahanced算法生成下一个切片的差值。 127 | 'hls_mount'=>'[vhost]/[app]/[stream].m3u8;',//内存HLS的M3u8/ts挂载点,和http_remux的mount含义一样 128 | 'hls_acodec'=>'libfdk_aac;',//默认的音频编码。当流的编码改变时,会更新PMT/PAT信息;默认是aac,因此默认的PMT/PAT信息是aac;如果流是mp3,那么可以配置这个参数为mp3,避免PMT/PAT改变。 129 | 'hls_vcodec'=>'h264;',//默认的视频编码。当流的编码改变时,会更新PMT/PAT信息;默认是h264。如果是纯音频HLS,可以配置为vn,可以减少SRS检测纯音频的时间,直接进入纯音频模式。 130 | 'hls_cleanup'=>'on;',//是否删除过期的ts切片,不在hls_window中就是过期。可以关闭清除ts切片,实现时移和存储,使用自己的切片管理系统。 131 | 'hls_nb_notify'=>'64;',//从notify服务器读取数据的长度 132 | 'hls_wait_keyframe'=>'on;',//是否按top切片,即等待到关键帧后开始切片。测试发现OS X和android上可以不用按go切片。 133 | ] 134 | ] 135 | ], 136 | ]; -------------------------------------------------------------------------------- /dev.php: -------------------------------------------------------------------------------- 1 | "EasySwoole", 4 | 'MAIN_SERVER' => [ 5 | 'LISTEN_ADDRESS' => '0.0.0.0', 6 | 'PORT' => 8080, 7 | 'SERVER_TYPE' => EASYSWOOLE_WEB_SERVER, //可选为 EASYSWOOLE_SERVER EASYSWOOLE_WEB_SERVER EASYSWOOLE_WEB_SOCKET_SERVER,EASYSWOOLE_REDIS_SERVER 8 | 'SOCK_TYPE' => SWOOLE_TCP, 9 | 'RUN_MODEL' => SWOOLE_PROCESS, 10 | 'SETTING' => [ 11 | 'worker_num' => 1, 12 | 'reload_async' => true, 13 | 'max_wait_time'=>3, 14 | 'enable_coroutine'=>true, 15 | 'enable_static_handler'=>true,//静态文件处理 16 | 'document_root' => EASYSWOOLE_ROOT.'/public', //静态文件目录 v4.4.0以下版本, 此处必须为绝对路径 17 | ], 18 | 'TASK'=>[ 19 | 'workerNum'=>0, 20 | 'maxRunningNum'=>128, 21 | 'timeout'=>15 22 | ] 23 | ], 24 | 'TEMP_DIR' => __DIR__.DIRECTORY_SEPARATOR.'storage'.DIRECTORY_SEPARATOR.'temp', 25 | 'LOG_DIR' => __DIR__.DIRECTORY_SEPARATOR.'storage'.DIRECTORY_SEPARATOR.'log', 26 | ]; 27 | 28 | -------------------------------------------------------------------------------- /easyswoole: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run($args); 25 | if(!empty($ret)){ 26 | echo $ret."\n"; 27 | } -------------------------------------------------------------------------------- /produce.php: -------------------------------------------------------------------------------- 1 | "EasySwoole", 4 | 'MAIN_SERVER' => [ 5 | 'LISTEN_ADDRESS' => '0.0.0.0', 6 | 'PORT' => 9501, 7 | 'SERVER_TYPE' => EASYSWOOLE_WEB_SERVER, //可选为 EASYSWOOLE_SERVER EASYSWOOLE_WEB_SERVER EASYSWOOLE_WEB_SOCKET_SERVER,EASYSWOOLE_REDIS_SERVER 8 | 'SOCK_TYPE' => SWOOLE_TCP, 9 | 'RUN_MODEL' => SWOOLE_PROCESS, 10 | 'SETTING' => [ 11 | 'worker_num' => 8, 12 | 'reload_async' => true, 13 | 'max_wait_time'=>3 14 | ], 15 | 'TASK'=>[ 16 | 'workerNum'=>4, 17 | 'maxRunningNum'=>128, 18 | 'timeout'=>15 19 | ] 20 | ], 21 | 'TEMP_DIR' => null, 22 | 'LOG_DIR' => null 23 | ]; 24 | -------------------------------------------------------------------------------- /storage/setup/srs2.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tenry18/live/d5489793f20282298f38d6a8d5a57c7f5bec5c84/storage/setup/srs2.0.zip --------------------------------------------------------------------------------