兰台 2024-05-30 15:24 采纳率: 0%
浏览 14
已结题

php 将rtmp协议转hls协议,无法播放

使用php编写直播服务器的时候,将rtmp协议转换位hls协议,生成ts文件无法播放,以下是hls协议的全部代码。项目在github上 rtmp_server,有没有人来修改一下代码呢完成了追加酬金

<?php

namespace Root;

use MediaServer\MediaReader\MediaFrame;

/**
 * @purpose hls协议测试版
 * @comment  生成ts文件无法播放,执行本项目下面的 php check.php 可以检查文件,发现文件有两个pid= 0 和pid =17
 * 导致无法播放。使用ffmpeg检查ffmpeg -i segment0.ts output.mp4,不会报错。但是显示没有任何媒体信息,那么ts文件里面没有可读的数据。
 * 但是生成的ts文件看着和ffmpeg生成差不多,但是都是乱码,看不明白,有什么软件能查看吗?有懂得朋友帮忙修改一下
* 代码地址:https://github.com/2723659854/rtmp_server
 */
class HlsDemo
{
    /** 切片时间 3秒 */
    public static $duration = 3;

    /**
     * 创建ts头
     * @param int $pid
     * @param $payload_unit_start_indicator
     * @param $continuity_counter
     * @param $adaptation_field_control
     * @return string
     */
    public static function createTsHeader(int $pid, $payload_unit_start_indicator = 0, $continuity_counter = 0, $adaptation_field_control = 1)
    {
        $sync_byte = 0x47;
        $header = chr($sync_byte);
        $header .= chr((($payload_unit_start_indicator << 6) | ($pid >> 8)) & 0xFF);
        $header .= chr($pid & 0xFF);
        $header .= chr(($adaptation_field_control << 4) | ($continuity_counter & 0x0F));
        return $header;
    }

    /**
     * 创建pes头
     * @param $stream_id
     * @param $payload
     * @param $pts
     * @param $dts
     * @return string
     */
    public static function createPes($stream_id, $payload, $pts, $dts = null)
    {
        $pes_start_code = "\x00\x00\x01";
        $pes_packet_length = 3 + 5 + strlen($payload) + ($dts !== null ? 5 : 0);
        $flags = 0x80;
        $header_data_length = 5;
        if ($dts !== null) {
            $flags |= 0x40;
            $header_data_length += 5;
        }

        $pes_header = $pes_start_code;
        $pes_header .= chr($stream_id);
        $pes_header .= chr($pes_packet_length >> 8);
        $pes_header .= chr($pes_packet_length & 0xFF);
        $pes_header .= chr(0x80);
        $pes_header .= chr($flags);
        $pes_header .= chr($header_data_length);

        $pes_header .= chr(($pts >> 29) & 0x0E | 0x21);
        $pes_header .= chr(($pts >> 22) & 0xFF);
        $pes_header .= chr(($pts >> 14) & 0xFE | 0x01);
        $pes_header .= chr(($pts >> 7) & 0xFF);
        $pes_header .= chr(($pts << 1) & 0xFE | 0x01);

        if ($dts !== null) {
            $pes_header .= chr(($dts >> 29) & 0x0E | 0x11);
            $pes_header .= chr(($dts >> 22) & 0xFF);
            $pes_header .= chr(($dts >> 14) & 0xFE | 0x01);
            $pes_header .= chr(($dts >> 7) & 0xFF);
            $pes_header .= chr(($dts << 1) & 0xFE | 0x01);
        }

        return $pes_header . $payload;
    }

    /**
     * 创建pat包
     * @param $pmtPid
     * @return string
     * @note 节目关联表:主要的作用就是指明了 PMT 表的 PID 值。
     */
    public static function createPatPacket($pmtPid)
    {
        $pat = "\x00\xB0\x0D\x00\x01\xC1\x00\x00\x00\x01" . chr($pmtPid >> 8) . chr($pmtPid & 0xFF) . "\x00\x00";
        return str_pad($pat, 188, chr(0xFF));
    }

    /**
     * 创建pmt包
     * @param int $pcr_pid
     * @param int $video_pid
     * @param int $audio_pid
     * @return string
     * @note 节目映射表:主要的作用就是指明了音视频流的 PID 值。
     */
    public static function createPmtPacket(int $pcr_pid, int $video_pid, int $audio_pid)
    {
        $pmt = "\x02\xB0\x17\x00\x01\xC1\x00\x00";
        $pmt .= chr($pcr_pid >> 8) . chr($pcr_pid & 0xFF) . "\xF0\x00";
        $pmt .= "\x1B" . chr($video_pid >> 8) . chr($video_pid & 0xFF) . "\xF0\x00";
        $pmt .= "\x0F" . chr($audio_pid >> 8) . chr($audio_pid & 0xFF) . "\xF0\x00";
        return str_pad($pmt, 188, chr(0xFF));
    }

    /**
     * 创建网络信息包
     * @return string
     */
    public static function createNitPacket()
    {
        $nit = "\x40\xF0\x11\x00\x01\xC1\x00\x00\x00\x01\xC1\x00\x00\x00\x01\xC1\x00\x00";
        return str_pad($nit, 188, chr(0xFF));
    }

    /**
     * 打包ts文件
     * @param int $pid
     * @param string $payload
     * @param $fileHandle
     * @param int $continuity_counter
     * @param int $payload_unit_start_indicator
     * @param int $adaptation_field_control
     * @return void
     */
    public static function writeTsPacket(int $pid, string $payload, $fileHandle, int &$continuity_counter, int $payload_unit_start_indicator = 0, int $adaptation_field_control = 1)
    {
        $packetSize = 188;
        $payloadSize = $packetSize - 4;
        $dataLen = strlen($payload);
        $i = 0;
        while ($i < $dataLen) {
            $header = self::createTsHeader($pid, ($i == 0) ? $payload_unit_start_indicator : 0, $continuity_counter, $adaptation_field_control);
            $continuity_counter = ($continuity_counter + 1) % 16;

            $chunk = substr($payload, $i, $payloadSize);
            $i += $payloadSize;

            if (strlen($chunk) < $payloadSize) {
                $chunk = str_pad($chunk, $payloadSize, chr(0xFF));
            }
            $packet = $header . $chunk;
            fwrite($fileHandle, $packet);
        }
    }

    /**
     * 将媒体数据打包并生成索引文件
     * @param MediaFrame $frame
     * @param string $playStreamPath
     * @return void
     * @note 协议入口
     */
    public static function make(MediaFrame $frame, string $playStreamPath)
    {
        /** ts存放路径 */
        $outputDir = app_path($playStreamPath);
        /** 切片时间 */
        $segmentDuration = self::$duration;
        /** 当前时间 */
        $nowTime = time();
        /** 媒体数据key */
        $mediaKey = $playStreamPath . '_media';
        /** 切片操作时间key */
        $lastCutTimeKey = $playStreamPath . '_time';
        /** 切片目录key */
        $tsFilesKey = $playStreamPath . '_ts';
        /** 将媒体数据投入到缓存 */
        Cache::push($mediaKey, $frame);
        /** 获取最近一次切片时间 */
        if (Cache::has($lastCutTimeKey)) {
            $lastCutTime = Cache::get($lastCutTimeKey);
        } else {
            $lastCutTime = $nowTime;
            Cache::set($lastCutTimeKey, $nowTime);
        }
        /** 比较当前时间和最近一次切片操作时间 若超过切片时间,则开始本次切片  */
        if (($nowTime - $lastCutTime) >= $segmentDuration) {
            /** 获取所有媒体数据 */
            $mediaData = Cache::flush($mediaKey);
            /** 并更写最近一次切片操作时间 */
            Cache::set($lastCutTimeKey, $nowTime);
        } else {
            /** 否则不操作 */
            return;
        }
        /** 创建目录 */
        if (!is_dir($outputDir)) {
            @mkdir($outputDir, 0777, true);
        }
        /**
         * $continuity_counter参数用于跟踪传输流中连续包的计数器。在传输流中,每个包都有一个序列号(0-15),
         * 用于确保数据包的顺序和完整性。$continuity_counter的作用是确保连续的传输包具有正确的序列号,以满足MPEG-TS协议的要求。
         */
        $continuity_counter = 0;
        /** 获取所有ts目录 */
        $tsFiles = Cache::flush($tsFilesKey);
        /** 生成ts名称 */
        $tsFile = 'segment' . count($tsFiles) . '.ts';
        /** ts存放路径 */
        $tsFileName = $outputDir . '/' . $tsFile;
        /** 打开ts文件 */
        $fileHandle = @fopen($tsFileName, 'wb');
        /** 写入pat包,指定pmt的pid */
        $patPacket = self::createPatPacket(4096); // PMT 的 PID 设置为 4096
        self::writeTsPacket(0, $patPacket, $fileHandle, $continuity_counter, 1);
        /** 创建pmt包 ,指定音视频媒体ID ,视频的pid是256 ,音频是257,pcr_id 同步流ID一般和视频一致 */
        $pmtPacket = self::createPmtPacket(256, 256, 257);
        self::writeTsPacket(4096, $pmtPacket, $fileHandle, $continuity_counter, 1);
        /** 创建网络NIT 包*/
        $nitPacket = self::createNitPacket();
        self::writeTsPacket(17, $nitPacket, $fileHandle, $continuity_counter, 1);

        /** 循环处理音视频数据信息 */
        foreach ($mediaData as $data) {
            /** 视频数据 */
            if ($data->FRAME_TYPE == MediaFrame::VIDEO_FRAME) {
                /** 播放时间戳 */
                $pts = $data->timestamp; // 确保PTS单位是90kHz
                /** 解码时间戳 */
                $dts = $pts;
                /** 视频帧本身就是被打包的es数据 */
                $videoEs = $data->getAVCPacket()->stream->dump();
                /** 打包成pes */
                $videoPes = self::createPes(0xE0, $videoEs, $pts, $dts);
                /** 打包成ts包 */
                self::writeTsPacket(256, $videoPes, $fileHandle, $continuity_counter);
            } elseif ($data->FRAME_TYPE == MediaFrame::AUDIO_FRAME) {
                /** 音频只有播放时间戳 */
                $pts = $data->timestamp; // 确保PTS单位是90kHz
                /** 音频本身就是被打包成es的包 */
                $audioEs = $data->getAACPacket()->stream->dump();
                /** 打包成pes */
                $audioPes = self::createPes(0xC0, $audioEs, $pts);
                /** 写入到ts包 */
                self::writeTsPacket(257, $audioPes, $fileHandle, $continuity_counter);
            } elseif ($data->FRAME_TYPE == MediaFrame::META_FRAME) {
                // 处理META_FRAME数据
                $metaEs = $data->dump();
                $metaPes = self::createPes(0xFC, $metaEs, $data->timestamp);
                self::writeTsPacket(258, $metaPes, $fileHandle, $continuity_counter, 1);
            }
        }
        /** 关闭ts文件 */
        @fclose($fileHandle);
        /** 将ts文件追加到目录 */
        $tsFiles[] = $tsFile;
        /** 生成索引文件 */
        self::generateM3U8($tsFiles, $outputDir);
        /** 将目录重新存入到缓存 */
        foreach ($tsFiles as $fileName) {
            Cache::push($tsFilesKey, $fileName);
        }
    }

    /**
     * 生成索引文件
     * @param array $tsFiles
     * @param string $outputDir
     * @return void
     */
    public static function generateM3U8(array $tsFiles, string $outputDir)
    {
        $m3u8Content = "#EXTM3U\n";
        $m3u8Content .= "#EXT-X-VERSION:3\n";
        $m3u8Content .= "#EXT-X-TARGETDURATION:3\n";
        $m3u8Content .= "#EXT-X-MEDIA-SEQUENCE:0\n";
        foreach ($tsFiles as $tsFile) {
            $m3u8Content .= "#EXTINF:3.000,\n";
            $m3u8Content .= $tsFile . "\n";
        }
        $m3u8Content .= "#EXT-X-ENDLIST\n";
        file_put_contents($outputDir . '/playlist.m3u8', $m3u8Content);
    }

    /**
     * 清空数据
     * @param string $playStreamPath
     * @return void
     */
    public static function close(string $playStreamPath)
    {
        /** 媒体数据key */
        $mediaKey = $playStreamPath . '_media';
        Cache::clear($mediaKey);
        /** 切片操作时间key */
        $lastCutTimeKey = $playStreamPath . '_time';
        Cache::clear($lastCutTimeKey);
        /** 切片目录key */
        $tsFilesKey = $playStreamPath . '_ts';
        Cache::clear($tsFilesKey);
    }
}



  • 写回答

20条回答 默认 最新

  • 阿里嘎多学长 2024-05-30 15:24
    关注

    以下内容由CHATGPT及阿里嘎多学长共同生成、有用望采纳:


    您好,根据您提供的代码和描述,我尝试提供一些可能的解决方案和建议:

    1. 检查PID值:确保您的TS流中使用的PID值是唯一的,并且符合MPEG-TS标准。PID 0通常用于网络信息表(NIT),而17是网络分配表(NIT)的一个常见PID。您的代码中,NIT的PID设置为17,这是正确的,但您需要确保其他流(如视频和音频)使用不同的PID值。

    2. 检查时间戳(PTS/DTS):确保视频和音频帧的时间戳(PTS/DTS)是正确的。如果时间戳不正确,可能会导致播放时出现问题。

    3. 检查媒体数据:您提到使用ffmpeg检查时没有报错,但没有媒体信息。这可能意味着媒体数据没有正确地封装到TS包中。检查您的MediaFrame类和相关的封装逻辑,确保音视频数据被正确处理和封装。

    4. 使用专用工具检查TS文件:可以使用如mp4box(来自GPAC项目)这样的工具来检查和分析TS文件。mp4box可以提供关于TS文件结构和流信息的详细报告。

    5. 参考文档和资源

    6. 代码审查和调试:仔细审查您的代码,特别是与封装音视频帧到TS包相关的部分。使用调试工具或添加日志输出,以帮助定位问题。

    7. 社区和论坛:考虑在相关的开发者社区或论坛(如Stack Overflow)发布您的问题,那里可能有更多有经验的开发者可以提供帮助。

    8. 示例代码:在GitHub或其他代码分享平台上搜索相关的PHP HLS转换库,可能会找到已经实现并经过测试的示例代码,这些代码可以作为参考或直接使用。

    希望这些建议对您有所帮助。如果您需要进一步的帮助,请提供更多的信息,例如错误日志、ffmpeg的输出结果等,这将有助于更准确地诊断问题。

    评论 编辑记录

报告相同问题?

问题事件

  • 已结题 (查看结题原因) 6月2日
  • 修改了问题 5月30日
  • 修改了问题 5月30日
  • 修改了问题 5月30日
  • 展开全部

悬赏问题

  • ¥15 爬取豆瓣电影相关处理
  • ¥15 手机淘宝抓清除消息接口
  • ¥15 C#无selenium
  • ¥15 LD衰减计算的结果过大
  • ¥15 用机器学习方法帮助保险公司预测哪些是欺诈行为
  • ¥15 计算300m以内的LD衰减
  • ¥15 数据爬取,python
  • ¥15 怎么看 cst中一个面的功率分布图,请说明详细步骤。类似下图
  • ¥15 为什么我的pycharm无法用pyqt6的QtWebEngine
  • ¥15 FOR循环语句显示查询超过300S错误怎么办