用 GStreamer 使用 HLS 快速搭建直播系統(tǒng)

前提條件是要先安裝 gstreamer陶衅, 我使用的是 macbook air, 具體的安裝步驟不在這里贅述策橘,
請參見官方文檔 Installing GStreamer

快速開始

其實步驟很簡單

  1. 簡單測試一下,確保你安裝的 gstreamer 工作正常应媚,并能從攝像頭中讀取視頻
  • 將攝像頭視頻顯示出來
gst-launch-1.0 avfvideosrc device-index=1 ! \
video/x-raw,width=1920,height=1080,format=UYVY,framerate=30/1 ! autovideosink

注: 可以通過 gst-device-monitor-1.0 命令來察看你的攝像頭的 device-index

  1. 錄制攝像頭視頻到 m3u8 和 ts 文件中
gst-launch-1.0 avfvideosrc device-index=1 ! x264enc ! h264parse ! hlssink2 max-files=10 location=./record_%05d.ts playlist-location=./playlist.m3u8

在 linux 系統(tǒng)中可使用

gst-launch-1.0 -v v4l2src device=/dev/video1 ! decodebin ! videoconvert ! omxh264enc ! h264parse ! hlssink2 max-files=10 location=./record_%05d.ts playlist-location=./playlist.m3u8 
  1. 顯示所錄制的視頻文件
gst-play-1.0 playlist.m3u8
  • 遠(yuǎn)程直播可以通過 web server, 例如 nginx, apache 等
    這里使用 "python3 -m http.server" 啟動一個測試的服務(wù)器
    在另外一臺電腦上訪問 http://ip:port/playlist.m3u8,就能看到直播的視頻了

講到這里就完了,就這么簡單鹦筹,如果你不想寫程序,就不用往下看了
如果你有興趣自己寫程序來完成上述步驟址貌,那我們可以繼續(xù)講講相關(guān)的代碼铐拐。

hlssink2 plugin

這里主要用到了 Gstreamer 的 hlssink2 插件, 其源代碼參見 hlssink2 source code

hlssink2 與采用復(fù)用 MPEG-TS 流作為輸入的舊 hlssink 不同练对,該元素采用基本音頻和視頻流作為輸入并在內(nèi)部處理復(fù)用遍蟋。 這使得 hlssink2 能夠就何時啟動新片段做出更好的決策,并且還可以更好地處理輸入流螟凭,而且如果其上游沒有 encoder element, 還可以根據(jù)需要生成關(guān)鍵幀虚青。

hlssink2 元素僅將 TS 片段文件 和 playlist 播放列表文件寫入指定目錄,它不包含實際的 HTTP 服務(wù)器來服務(wù)這些文件螺男。 只需將外部網(wǎng)絡(luò)服務(wù)器指向包含播放列表和片段文件的目錄即可棒厘。

example

我們可以用 C++ 語言簡單寫一個例子,就三個文件

  1. hls-exam.cpp // 測試 gstreamer hlssink2 的代碼
  2. pipeline_controller.h // 構(gòu)建 gstreamer pipeline 的接口文件
  3. pipeline_controller.cpp //構(gòu)建 gstreamer pipeline 的實現(xiàn)文件
#include <chrono>
#include <thread>
#include "pipeline_controller.h"

int main(int argc, char *argv[]) {
    auto controller = std::make_unique<PipelineController>();
    controller->init(argc, argv);
    controller->start();
    std::this_thread::sleep_for(std::chrono::seconds(10));
    controller->pause();
    std::this_thread::sleep_for(std::chrono::seconds(10));
    controller->resume();
    controller->stop();
    controller->clean();
}
#pragma once

#include <gst/gst.h>
#include <glib.h>
#include <string>
#include <map>

class PipelineController {
public:
    PipelineController();
    virtual ~PipelineController();
    int init(int argc, char *argv[]);
    int clean();

    int start();
    int stop();

    int pause();
    int resume();

private:
    bool create_elements();
    bool link_elements();
    void unlink_elements();

    GstElement* create_element(const std::string& factory, const std::string& name);
    int setup_elements();

    std::string m_video_source;
    std::string m_video_target;

    GstElement* m_source_element;
    GstElement* m_target_element;
    GstElement* m_tee_element;
    GstElement* m_enc_element;

    std::map<std::string, GstElement*> m_elements;
    
    GMainLoop* m_loop;
    GstElement* m_pipeline;
    GstBus* m_bus;
    gulong m_probe_id;
};

#include <iostream>
#include <chrono>  // chrono::system_clock
#include <ctime>   // localtime
#include <sstream> // stringstream
#include <iomanip> // put_time
#include <string>  // string
#include <fmt/core.h>
#include <chrono>
#include <thread>
#include <gst/gst.h>
#include <glib.h>
#include "pipeline_controller.h"

#define PAD_NAME "video"
#define TIME_FMT "%Y%m%d%H%M%S"
#define DEBUG_TRACE(msg) std::cout << "[" \
    << time(NULL) <<","<< __FILE_NAME__ << "," << __LINE__ << "]\t"<< msg << std::endl


static const GstPadProbeType pad_probe_type = GST_PAD_PROBE_TYPE_BLOCK_DOWNSTREAM;

static uint32_t deleted_fragments = 0;

bool has_option(
    const std::vector<std::string_view>& args, 
    const std::string_view& option_name) {
    for (auto it = args.begin(), end = args.end(); it != end; ++it) {
        if (*it == option_name)
            return true;
    }
    
    return false;
}

std::string_view get_option(
    const std::vector<std::string_view>& args, 
    const std::string_view& option_name) {
    for (auto it = args.begin(), end = args.end(); it != end; ++it) {
        if (*it == option_name)
            if (it + 1 != end)
                return *(it + 1);
    }
    
    return "";
}

std::string get_time_str(
    const std::chrono::system_clock::time_point& timePoint, 
    const std::string& strPattern)
{
    auto in_time_t = std::chrono::system_clock::to_time_t(timePoint);

    std::stringstream ss;
    ss << std::put_time(std::localtime(&in_time_t), TIME_FMT);
    return fmt::format(fmt::runtime(strPattern), ss.str());
}

static void check_pads(GstElement *element) {
    GstIterator *iter = gst_element_iterate_pads(element);
    GValue *elem;
    
    while (gst_iterator_next(iter, elem) == GST_ITERATOR_OK) {
        gchar * strVal = g_strdup_value_contents (elem);
        DEBUG_TRACE("pad: " << strVal);
        free (strVal);
    }
    gst_iterator_free(iter);
}


static gboolean delete_fragment_callback(GstElement *element, const gchar *uri, gpointer user_data) {
    // Your custom logic for handling fragment deletion here.
    // In this example, we will simply print a message.
    DEBUG_TRACE(++deleted_fragments << ". Deleted fragment: " << uri);
    return TRUE;
}

static GstPadProbeReturn block_downstream_probe(GstPad *pad, GstPadProbeInfo *info, gpointer user_data) {
    // Block the downstream data flow by returning FALSE in the probe function.
    DEBUG_TRACE("blocking stream...");
    return GST_PAD_PROBE_OK;
}

PipelineController::PipelineController()
: m_loop(nullptr)
, m_pipeline(nullptr)
, m_bus(nullptr)
, m_probe_id(0) {
    DEBUG_TRACE("PipelineController construct");
}

PipelineController::~PipelineController()
{
    DEBUG_TRACE("PipelineController destruct");
}

int PipelineController::init(int argc, char *argv[]) {
    gst_init(&argc, &argv);
    DEBUG_TRACE("PipelineController init");
    const std::vector<std::string_view> args(argv, argv + argc);
    const std::string_view video_source_plugin = get_option(args, "-s");
    const std::string_view video_target_plugin = get_option(args, "-t");

    m_video_source = "videotestsrc";
    m_video_target = "hlssink2";

    if (!video_source_plugin.empty()) {
        m_video_source = video_source_plugin;
    }

    if (!video_target_plugin.empty()) {
        m_video_target = video_target_plugin;
    }
    create_elements();
    setup_elements();
    link_elements();
    return 0;
}
int PipelineController::clean() {
    DEBUG_TRACE("PipelineController clean");
   
    //gst_object_unref to free pipeline resources including all added GstElement objects
    gst_object_unref(m_pipeline);
    gst_object_unref(m_bus);

    return 0;
}

int PipelineController::start() {
    DEBUG_TRACE("PipelineController start");
    //check_pads(m_target_element);
    
    std::string dot_file = "video_pipeline";
    //set environment variable, such as export GST_DEBUG_DUMP_DOT_DIR=/tmp
    GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN_CAST(m_pipeline), GST_DEBUG_GRAPH_SHOW_VERBOSE, dot_file.c_str());

    DEBUG_TRACE("start playing...");
    gst_element_set_state(m_pipeline, GST_STATE_PLAYING);
    return 0;
}
int PipelineController::stop() {
    DEBUG_TRACE("stop playing...");
    gst_element_set_state(m_pipeline, GST_STATE_NULL);
    return 0;
}

int PipelineController::pause() {
    DEBUG_TRACE("pause playing...");
    m_probe_id = 0;
    // Get the source pad of hlssink
    GstPad *hlssink_pad = gst_element_get_static_pad(m_target_element, PAD_NAME);
    if(hlssink_pad) {
        DEBUG_TRACE("to block stream");
        m_probe_id = gst_pad_add_probe(hlssink_pad, GST_PAD_PROBE_TYPE_BLOCK_DOWNSTREAM, block_downstream_probe, NULL, NULL);
    }
    
    return m_probe_id;
}

int PipelineController::resume() {
    DEBUG_TRACE("resume playing...");
    if (!m_probe_id) {
        DEBUG_TRACE("have not paused");
        return -1;
    }
    GstPad *hlssink_pad = gst_element_get_static_pad(m_target_element, PAD_NAME);
    if(hlssink_pad) {
        DEBUG_TRACE("to unblock stream");
        gst_pad_remove_probe(hlssink_pad, m_probe_id);
        m_probe_id = 0;
    }
    
    return 0;
    
}


bool PipelineController::create_elements() {
    DEBUG_TRACE("PipelineController create_elements");
    m_pipeline = gst_pipeline_new("video-pipeline");
    m_bus = gst_element_get_bus(m_pipeline);
    m_source_element = create_element(m_video_source, "video-source");
    m_tee_element = create_element("tee", "video-tee");
    m_enc_element = create_element("x264enc", "video-encoder");
    m_target_element = create_element(m_video_target, "video-target");

    if(m_source_element && m_tee_element && m_enc_element && m_target_element) {
        return true;
    }
    return false;
}

int PipelineController::setup_elements() {
    DEBUG_TRACE("PipelineController setup_elements");
    if (m_video_source == "videotestsrc") {
        g_object_set(m_source_element, "pattern", 0, NULL); // Set the test pattern
    } else if(m_video_source == "avfvideosrc") {
        g_object_set(m_source_element, "device-index", 0, NULL); // Set the test pattern
        
        GstCaps* caps = gst_caps_new_simple("video/x-raw",       
            "width", G_TYPE_INT, 1920,                               
            "height", G_TYPE_INT, 1080,                              
            "framerate", GST_TYPE_FRACTION, 30, 1, NULL);
             
        g_object_set(G_OBJECT(m_source_element), "caps", caps, nullptr);
        gst_caps_unref(caps); 
        
    } else {
        DEBUG_TRACE("unknown source element");
    }

    auto now = std::chrono::system_clock::now();
    std::string playlist_filename = get_time_str(now, "/tmp/playlist_{}.m3u8");
    std::string record_filename = get_time_str(now, "/tmp/record_{}_%05d.ts");

    DEBUG_TRACE("playlist filename: " << playlist_filename 
        << ", record_filename=" << record_filename);

    g_object_set(m_target_element, "location", record_filename.c_str(), NULL);
    g_object_set(m_target_element, "playlist-location", playlist_filename.c_str(), NULL);
    //g_object_set(m_target_element, "playlist-root", "/tmp", NULL);
    g_object_set(m_target_element, "playlist-length", 20, NULL);
    g_object_set(m_target_element, "max-files", 20, NULL);
    g_object_set(m_target_element, "target-duration", 10, NULL);

    g_signal_connect(G_OBJECT(m_target_element), "delete-fragment", G_CALLBACK(delete_fragment_callback), NULL);
    return 0;
}

bool PipelineController::link_elements() {
    DEBUG_TRACE("add elements");
    gst_bin_add_many(GST_BIN(m_pipeline), m_source_element, m_tee_element, m_enc_element, m_target_element, NULL);
    DEBUG_TRACE("link_elements");
    gst_element_link_many(m_source_element, m_tee_element, m_enc_element, m_target_element, NULL);
    
    return true;
}
void PipelineController::unlink_elements() {
    DEBUG_TRACE("unlink_elements");
    gst_element_unlink_many(m_source_element, m_tee_element, m_enc_element, m_target_element, NULL);

    DEBUG_TRACE("remove elements");
    gst_bin_remove_many(GST_BIN(m_pipeline), m_source_element, m_tee_element, m_enc_element, m_target_element, NULL);
  
}

GstElement* PipelineController::create_element(
    const std::string& factory, 
    const std::string& name) {
    DEBUG_TRACE("create_element:" << factory << ", name=" << name);
    GstElement* e = gst_element_factory_make(factory.c_str(), name.c_str());
    m_elements.emplace(std::make_pair(name, e));
    return e;
}

代碼放置于 https://github.com/walterfan/gstreamer-cookbook
測試代碼會產(chǎn)生

  • 一個 playlist_20231019233046.m3u8 文件
  • 若干個 /tmp/record_20231019233046_xxx.ts 文件

playlist_20231019233046.m3u8 的內(nèi)容如下

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:NO
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:10

#EXTINF:10,
record_20231019233046_00000.ts
#EXTINF:10,
record_20231019233046_00001.ts
#EXTINF:10,
record_20231019233046_00002.ts
...
#EXT-X-ENDLIST

控制臺輸出如下

./example/hls-exam
[1697729446,pipeline_controller.cpp,91] PipelineController construct
[1697729446,pipeline_controller.cpp,101]    PipelineController init
[1697729446,pipeline_controller.cpp,181]    PipelineController create_elements
[1697729446,pipeline_controller.cpp,252]    create_element:videotestsrc, name=video-source
[1697729446,pipeline_controller.cpp,252]    create_element:tee, name=video-tee
[1697729446,pipeline_controller.cpp,252]    create_element:x264enc, name=video-encoder
[1697729446,pipeline_controller.cpp,252]    create_element:hlssink2, name=video-target
[1697729446,pipeline_controller.cpp,196]    PipelineController setup_elements
[1697729446,pipeline_controller.cpp,219]    playlist filename: /tmp/playlist_20231019233046.m3u8, record_filename=/tmp/record_20231019233046_%05d.ts
[1697729446,pipeline_controller.cpp,233]    add elements
[1697729446,pipeline_controller.cpp,235]    link_elements
[1697729446,pipeline_controller.cpp,132]    PipelineController start
[1697729446,pipeline_controller.cpp,139]    start playing...
[1697729452,pipeline_controller.cpp,76] 1. Deleted fragment: /tmp/record_20231019233046_00000.ts
[1697729452,pipeline_controller.cpp,76] 2. Deleted fragment: /tmp/record_20231019233046_00001.ts
[1697729453,pipeline_controller.cpp,76] 3. Deleted fragment: /tmp/record_20231019233046_00002.ts
[1697729453,pipeline_controller.cpp,76] 4. Deleted fragment: /tmp/record_20231019233046_00003.ts
[1697729453,pipeline_controller.cpp,76] 5. Deleted fragment: /tmp/record_20231019233046_00004.ts
[1697729453,pipeline_controller.cpp,76] 6. Deleted fragment: /tmp/record_20231019233046_00005.ts
[1697729454,pipeline_controller.cpp,76] 7. Deleted fragment: /tmp/record_20231019233046_00006.ts
[1697729454,pipeline_controller.cpp,76] 8. Deleted fragment: /tmp/record_20231019233046_00007.ts
[1697729454,pipeline_controller.cpp,76] 9. Deleted fragment: /tmp/record_20231019233046_00008.ts
[1697729455,pipeline_controller.cpp,76] 10. Deleted fragment: /tmp/record_20231019233046_00009.ts
[1697729455,pipeline_controller.cpp,76] 11. Deleted fragment: /tmp/record_20231019233046_00010.ts
[1697729455,pipeline_controller.cpp,76] 12. Deleted fragment: /tmp/record_20231019233046_00011.ts
[1697729455,pipeline_controller.cpp,76] 13. Deleted fragment: /tmp/record_20231019233046_00012.ts
[1697729456,pipeline_controller.cpp,150]    pause playing...
[1697729456,pipeline_controller.cpp,155]    to block stream
[1697729456,pipeline_controller.cpp,82] blocking stream...
[1697729466,pipeline_controller.cpp,163]    resume playing...
[1697729466,pipeline_controller.cpp,170]    to unblock stream
[1697729466,pipeline_controller.cpp,144]    stop playing...
[1697729466,pipeline_controller.cpp,122]    PipelineController clean
[1697729466,pipeline_controller.cpp,96] PipelineController destruct
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子矛双,更是在濱河造成了極大的恐慌,老刑警劉巖达传,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異迫筑,居然都是意外死亡宪赶,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門脯燃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來搂妻,“玉大人,你說我怎么就攤上這事辕棚∮鳎” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵逝嚎,是天一觀的道長扁瓢。 經(jīng)常有香客問我,道長补君,這世上最難降的妖魔是什么引几? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮挽铁,結(jié)果婚禮上伟桅,老公的妹妹穿的比我還像新娘敞掘。我一直安慰自己,他們只是感情好楣铁,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布玖雁。 她就那樣靜靜地躺著,像睡著了一般民褂。 火紅的嫁衣襯著肌膚如雪茄菊。 梳的紋絲不亂的頭發(fā)上疯潭,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天赊堪,我揣著相機與錄音,去河邊找鬼竖哩。 笑死哭廉,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的相叁。 我是一名探鬼主播遵绰,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼增淹!你這毒婦竟也來了椿访?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤虑润,失蹤者是張志新(化名)和其女友劉穎成玫,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拳喻,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡哭当,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了冗澈。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钦勘。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖亚亲,靈堂內(nèi)的尸體忽然破棺而出彻采,到底是詐尸還是另有隱情,我是刑警寧澤捌归,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布肛响,位于F島的核電站,受9級特大地震影響陨溅,放射性物質(zhì)發(fā)生泄漏终惑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一门扇、第九天 我趴在偏房一處隱蔽的房頂上張望雹有。 院中可真熱鬧偿渡,春花似錦、人聲如沸霸奕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽质帅。三九已至适揉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間煤惩,已是汗流浹背嫉嘀。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留魄揉,地道東北人剪侮。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像洛退,于是被迫代替她去往敵國和親瓣俯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容