流媒体主要有两种应用场景,即直播和点播:
- 直播:服务端实时发送直播来源(如系统桌面、摄像头)的数据流,客户端通过支持流媒体协议的播放器实时播放同样的内容,不可拖动进度。
- 点播:服务端存放多个视频文件,客户端可通过网路点播客户端任意观看其中一个视频,并可拖动进度进行观看。
流媒体技术能提供诸如视频加密和播放体验大幅提升等优点。
协议的选择
流媒体播放有两种协议可供选择:HLS 和 RMTP。
- HLS,是苹果公司实现的基于 HTTP 的流媒体传输协议,全称 HTTP Live Streaming,可支持流媒体的直播和点播,主要应用在 iOS 系统,为 iOS 设备(如 iPhone、iPad)提供音视频直播和点播方案。
- RTMP,实时消息传输协议,Real Time Messaging Protocol,是 Adobe Systems 公司为 Flash 播放器和服务器之间音频、视频和数据传输开发的开放协议。协议基于 TCP,是一个协议族,包括 RTMP 基本协议及 RTMPT/RTMPS/RTMPE 等多种变种。RTMP 是一种设计用来进行实时数据通信的网络协议,主要用来在 Flash/AIR 平台和支持RTMP协议的流媒体/交互服务器之间进行音视频和数据通信。
在web点播业务中选用HLS协议,两种协议的详细介绍和选择HLS的原因会在下文给出。
HLS协议
HLS协议规范文档如下:https://tools.ietf.org/html/rfc8216。
HLS 数据通过 HTTP 协议传输,在H5中可以很容易得到支持,具有良好的兼容性。 HLS 的基本原理就是将视频分割为多个TS格式的文件片段存储在服务器,同时需要建立一个 m3u8 的索引文件来维护所有的 TS 片段的索引。客户端播放视频时,它是从 m3u8 索引文件获取 TS 视频文件片段来播放。相对于常见的流媒体播放协议,例如 RTMP 协议、RTSP 协议等,HLS 最大的不同在于直播客户端获取到的并不是一个完整的数据流,而是连续的、短时长的媒体文件,客户端不断的下载并播放这些小文件。每个 TS 文件的时长并无强制规定,可以根据需求自由分割,推荐是 5-10 秒一个分片。
值得一提的是,由于 HLS 的分段策略,如果应用于直播,则理论最小延时为一个 TS 文件的时长,一般情况为 2-3 个 ts 文件的时长,通常 HLS 在直播场景可能的延时会达到 20-30s,而高延时对于需要实时互动体验的直播来说是不可接受的。这就意味着,如果直播过程中需要双向互动,则HLS不太适合。当然,对于点播来说,则无需考虑上述问题。
HLS 使用短时长的分片文件来播放,客户端可以平滑的切换码率,以适应不同带宽条件下的播放。
HLS原理示意图如下:
RTMP 协议
相对于 HLS 来说,采用 RTMP 协议时,对于直播场景,从采集推流端到流媒体服务器再到播放端是一条数据流,因此在服务器不会有落地文件。相对来说,延时较小,通常为 1-3s,参考播放器 如ijkplayer、毫秒级的播放器,可以参考大牛直播SDK的RTMP播放器。
因此业界大部分直播业务都会选择用 RTMP 作为流媒体协议。
但是RTMP也有一些问题需要解决,浏览器无法原生支持该协议,如要在web中应用,就需要开发支持相关协议的播放器。因此该协议多应用 native 环境。
综上,针对点播业务场景,HLS显然是最合适的选择。
m3u8文件格式详解
m3u8 文件本质上是一个文本文件,下载上述示例文件(test.m3u8),并用文本编辑器打开,内容如下(部分):
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:3
#EXTINF:4.300000,
test3.ts
#EXTINF:4.133333,
test4.ts
#EXTINF:1.933333,
test0.ts
#EXTINF:2.700000,
test1.ts
#EXTINF:3.166667,
test2.ts
m3u8 用 UTF-8 编码, 实质是一个播放列表(playlist), 其内部信息记录的是一系列媒体片段资源,顺序播放该片段资源,即可完整展示多媒体资源。对于点播来说,客户端只需按顺序下载上述片段资源,依次进行播放即可。而对于直播来说,客户端需要定时重新请求该 m3u8 文件,看下是否有新的片段数据需要进行下载并播放。
需要注意,m3u8文件本身及TS文件,均需保证与当前域一致或者目标服务器能够支持当前域跨域。
环境
FFMPEG 版本
ffmpeg-4.3.1.tar.bz2
./configure --prefix=./install --enable-shared --disable-static --disable-x86asm
文件目录
├── 3rdparty
│ ├── FreeSerif
│ ├── ffmpeg
│ │ ├── include
│ │ │ ├── libavcodec
│ │ │ ├── libavdevice
│ │ │ ├── libavfilter
│ │ │ ├── libavformat
│ │ │ ├── libavutil
│ │ │ ├── libswresample
│ │ │ └── libswscale
│ │ └── lib
│ ├── opencv
│ └── spd
│ └── spdlog
│ ├── cfg
│ ├── details
│ ├── fmt
│ │ └── bundled
│ └── sinks
├── CMakeLists.txt
├── README.md
├── build
├── docs
│ ├── imgs
│ │ └── spdlog.png
│ └── spdlog.md
├── incs
│ ├── CGDesktopCatch.hpp
│ ├── CGUsbCamera.hpp
│ ├── common.hpp
│ └── mylog.hpp
├── srcs
│ ├── CGDesktopCatch.cpp
│ └── CGUsbCamera.cpp
└── test
├── CGDesktopCatchDemo.cpp
├── CMakeLists.txt
├── UsbCameraDemo.cpp
└── spddemo.cpp
common.hpp
#pragma once
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavdevice/avdevice.h"
#include "libavfilter/avfilter.h"
#include "libavfilter/buffersink.h"
#include "libavfilter/buffersrc.h"
#include "libavformat/avformat.h"
#include "libavformat/avio.h"
#include "libavutil/channel_layout.h"
#include "libavutil/common.h"
#include "libavutil/fifo.h"
#include "libavutil/imgutils.h"
#include "libavutil/mathematics.h"
#include "libavutil/opt.h"
#include "libavutil/samplefmt.h"
#include "libavutil/time.h"
#include "libswresample/swresample.h"
#include "libswscale/swscale.h"
}
#include "mylog.hpp"
#define RET_OK (0)
#define RET_FAILED (-1)
CGDesktopCatch.hpp
#pragma once
#include <iostream>
#include <memory>
#include <string>
#include <thread>
#include "common.hpp"
class SwsScaleContext
{
public:
SwsScaleContext() {
}
void SetSrcResolution(int width, int height)
{
srcWidth = width;
srcHeight = height;
}
void SetDstResolution(int width, int height)
{
dstWidth = width;
dstHeight = height;
}
void SetFormat(AVPixelFormat iformat, AVPixelFormat oformat)
{
this->iformat = iformat;
this->oformat = oformat;
}
public:
int srcWidth;
int srcHeight;
int dstWidth;
int dstHeight;
AVPixelFormat iformat;
AVPixelFormat oformat;
};
class CGDesktopCatch
{
public:
AVFormatContext *inputContext = nullptr;
AVCodecContext *encodeContext = nullptr;
AVFormatContext *outputContext = nullptr;
struct SwsContext *pSwsContext = nullptr;
uint8_t *pSwpBuffer = nullptr;
AVFrame *videoFrame = nullptr;
AVFrame *pSwsVideoFrame = nullptr;
SwsScaleContext swsScaleContext;
int64_t startTime;
int64_t lastReadPacktTime;
int64_t packetCount = 0;
std::thread m_hThread;
private:
bool m_bStoped{
true};
bool m_bInputInited{
false};
bool m_bOutputInited{
false};
std::shared_ptr<std::mutex> ctxLock = std::make_shared<std::mutex>();
public:
CGDesktopCatch();
~CGDesktopCatch();
void StartDecode();
void StopDecode();
private:
void Init();
void ReadingThrd(void *pParam);
void run();
int OpenInput(std::string inputUrl);
int OpenOutput(std::string outUrl, AVCodecContext *encodeCodec);
void CloseInput();
void CloseOutput();
void DecodeAndEncode();
int WritePacket(std::shared_ptr<AVPacket> packet);
bool Decode(AVStream *inputStream, AVPacket *packet, AVFrame *frame);
int InitDecodeContext(AVStream *inputStream);
int initSwsFrame(AVFrame *pSwsFrame, int iWidth, int iHeight);
int initEncoderCodec(AVStream *inputStream, AVCodecContext **encodeContext);
int initSwsContext(struct SwsContext **pSwsContext, SwsScaleContext *swsScaleContext);
std::shared_ptr<AVPacket> Encode(AVCodecContext *encodeContext, AVFrame *frame);
std::shared_ptr<AVPacket> ReadPacketFromSource();
};
CGDesktopCatch.cpp
#include "CGDesktopCatch.hpp"
#include <exception>
#include <iostream>
#include <memory>
#include <mutex>
#include <sstream>
#include <string>
#include <thread>
using namespace std;
static string AvErrToStr(int avErrCode)
{
const auto bufSize = 1024U;
char *errString = (char *) calloc(bufSize, sizeof(*errString));
if (!errString)
{