GStreamer 06: Basic tutorial 4

Time management

Posted by NAB on December 15, 2022

Goal

Bài hướng dẫn này giới thiệu cách để sử dụng các tiện ích liên quan đến thời gian của GStreamer, cụ thể như sau:

  • Cách truy vấn pipeline để biết thông tin như vị trí hoặc thời gian của luồng.
  • Cách tìm - seeks (nhảy - jump) đến các vị trí khác nhau trong luồng.

Introduction

GstQuery là một cơ chế cho phép yêu cầu một phần tử hoặc pad cho biết một phần thông tin. Trong ví dụ này, chúng ta yêu cầu pipeline có cho phép tìm kiếm hay không (một số source như live streams, không cho phép tìm kiếm). Nếu pipeline cho phép, khi phim đã chạy được 10 giây, chúng tôi chuyển sang vị trí khác bằng cách sử dụng tìm kiếm.

Trong các bài hướng dẫn trước, khi chúng ta đã thiết lập và chạy pipeline, chức năng chính của chúng ta chỉ là ngồi và chờ nhận ERROR hoặc EOS thông qua bus. Ở đây, chúng ta chỉnh sửa chức năng này để wake up định kỳ và query pipeline về vị trí luồng, do đó chúng ta có thể in chúng ra màn hình. Nó tương tự với việc mà các ứng dụng như media player sẽ làm, cập nhật giao diện người dùng định kỳ.

Cuối cùng, thời lượng của luồng được truy vấn và cập nhật khi nó thay đổi.

Seeking example

  • Copy đoạn code này vào file có tên basic_tutorial_4.c
#include <gst/gst.h>

/* Structure to contain all our information, so we can pass it around */
typedef struct _CustomData
{
    GstElement *playbin;   /* Our one and only element */
    gboolean playing;      /* Are we in the PLAYING state? */
    gboolean terminate;    /* Should we terminate execution? */
    gboolean seek_enabled; /* Is seeking enabled for this media? */
    gboolean seek_done;    /* Have we performed the seek already? */
    gint64 duration;       /* How long does this media last, in nanoseconds */
} CustomData;

/* Forward definition of the message processing function */
static void handle_message(CustomData *data, GstMessage *msg);

int main(int argc, char *argv[])
{
    CustomData data;
    GstBus *bus;
    GstMessage *msg;
    GstStateChangeReturn ret;

    data.playing = FALSE;
    data.terminate = FALSE;
    data.seek_enabled = FALSE;
    data.seek_done = FALSE;
    data.duration = GST_CLOCK_TIME_NONE;

    /* Initialize GStreamer */
    gst_init(&argc, &argv);

    /* Create the elements */
    data.playbin = gst_element_factory_make("playbin", "playbin");

    if (!data.playbin)
    {
        g_printerr("Not all elements could be created.\n");
        return -1;
    }

    /* Set the URI to play */
    g_object_set(data.playbin, "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL);

    /* Start playing */
    ret = gst_element_set_state(data.playbin, GST_STATE_PLAYING);
    if (ret == GST_STATE_CHANGE_FAILURE)
    {
        g_printerr("Unable to set the pipeline to the playing state.\n");
        gst_object_unref(data.playbin);
        return -1;
    }

    /* Listen to the bus */
    bus = gst_element_get_bus(data.playbin);
    do
    {
        msg = gst_bus_timed_pop_filtered(bus, 100 * GST_MSECOND,
                                         GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS | GST_MESSAGE_DURATION);

        /* Parse message */
        if (msg != NULL)
        {
            handle_message(&data, msg);
        }
        else
        {
            /* We got no message, this means the timeout expired */
            if (data.playing)
            {
                gint64 current = -1;

                /* Query the current position of the stream */
                if (!gst_element_query_position(data.playbin, GST_FORMAT_TIME, &current))
                {
                    g_printerr("Could not query current position.\n");
                }

                /* If we didn't know it yet, query the stream duration */
                if (!GST_CLOCK_TIME_IS_VALID(data.duration))
                {
                    if (!gst_element_query_duration(data.playbin, GST_FORMAT_TIME, &data.duration))
                    {
                        g_printerr("Could not query current duration.\n");
                    }
                }

                /* Print current position and total duration */
                g_print("Position %" GST_TIME_FORMAT " / %" GST_TIME_FORMAT "\r",
                        GST_TIME_ARGS(current), GST_TIME_ARGS(data.duration));

                /* If seeking is enabled, we have not done it yet, and the time is right, seek */
                if (data.seek_enabled && !data.seek_done && current > 10 * GST_SECOND)
                {
                    g_print("\nReached 10s, performing seek...\n");
                    gst_element_seek_simple(data.playbin, GST_FORMAT_TIME,
                                            GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, 30 * GST_SECOND);
                    data.seek_done = TRUE;
                }
            }
        }
    } while (!data.terminate);

    /* Free resources */
    gst_object_unref(bus);
    gst_element_set_state(data.playbin, GST_STATE_NULL);
    gst_object_unref(data.playbin);
    return 0;
}

static void handle_message(CustomData *data, GstMessage *msg)
{
    GError *err;
    gchar *debug_info;

    switch (GST_MESSAGE_TYPE(msg))
    {
    case GST_MESSAGE_ERROR:
        gst_message_parse_error(msg, &err, &debug_info);
        g_printerr("Error received from element %s: %s\n", GST_OBJECT_NAME(msg->src), err->message);
        g_printerr("Debugging information: %s\n", debug_info ? debug_info : "none");
        g_clear_error(&err);
        g_free(debug_info);
        data->terminate = TRUE;
        break;
    case GST_MESSAGE_EOS:
        g_print("\nEnd-Of-Stream reached.\n");
        data->terminate = TRUE;
        break;
    case GST_MESSAGE_DURATION:
        /* The duration has changed, mark the current one as invalid */
        data->duration = GST_CLOCK_TIME_NONE;
        break;
    case GST_MESSAGE_STATE_CHANGED:
    {
        GstState old_state, new_state, pending_state;
        gst_message_parse_state_changed(msg, &old_state, &new_state, &pending_state);
        if (GST_MESSAGE_SRC(msg) == GST_OBJECT(data->playbin))
        {
            g_print("Pipeline state changed from %s to %s:\n",
                    gst_element_state_get_name(old_state), gst_element_state_get_name(new_state));

            /* Remember whether we are in the PLAYING state or not */
            data->playing = (new_state == GST_STATE_PLAYING);

            if (data->playing)
            {
                /* We just moved to PLAYING. Check if seeking is possible */
                GstQuery *query;
                gint64 start, end;
                query = gst_query_new_seeking(GST_FORMAT_TIME);
                if (gst_element_query(data->playbin, query))
                {
                    gst_query_parse_seeking(query, NULL, &data->seek_enabled, &start, &end);
                    if (data->seek_enabled)
                    {
                        g_print("Seeking is ENABLED from %" GST_TIME_FORMAT " to %" GST_TIME_FORMAT "\n",
                                GST_TIME_ARGS(start), GST_TIME_ARGS(end));
                    }
                    else
                    {
                        g_print("Seeking is DISABLED for this stream.\n");
                    }
                }
                else
                {
                    g_printerr("Seeking query failed.");
                }
                gst_query_unref(query);
            }
        }
    }
    break;
    default:
        /* We should not reach here */
        g_printerr("Unexpected message received.\n");
        break;
    }
    gst_message_unref(msg);
}
  • Complie
gcc basic_tutorial_4.c -o basic_tutorial_4 `pkg-config --cflags --libs gstreamer-1.0`
  • Run
./basic_tutorial_4

Walkthrough

typedef struct _CustomData
{
    GstElement *playbin;
    gboolean playing;
    gboolean terminate;
    gboolean seek_enabled;
    gboolean seek_done; 
    gint64 duration;    
} CustomData;

static void handle_message(CustomData *data, GstMessage *msg);

Chúng ta bắt đầu bằng việc định nghĩa một structure để chứa các thông tin của chúng ta, do đó chúng ta có thể truyền chúng qua lại các hàm khác. Đặc biệt, trong ví dụ này, chúng ta chuyển code xử lý thông báo sang chức năng riêng của nó handle_message vì nó đang trở nên quá lớn một chút.

Sau đó chúng ta xây dựng một pipeline bao gồm nột phần tử duy nhất, một playbin, mà chúng ta đã thấy trong Basic tutorial 1. Tuy nhiên, bản thân playbin là một pipeline, và trong trường hợp này, nó sẽ là phần tử duy nhất trong pipeline, do đó chúng ta sử dụng phần tử playbin trực tiếp. Chúng ta sẽ bỏ qua các chi tiết: URI của clip được đưa vào playbin thông qua thuộc tính URI và pipeline được đặt sang trạng thái đang chạy PLAYING.

msg = gst_bus_timed_pop_filtered(bus, 100 * GST_MSECOND,
                                 GST_MESSAGE_STATE_CHANGED | 
                                 GST_MESSAGE_ERROR | 
                                 GST_MESSAGE_EOS | 
                                 GST_MESSAGE_DURATION);

Trước đây, chúng ta không cung cấp thời gian chờ cho gst_bus_timed_pop_filtered(), có nghĩa là nó không quay lại cho đến khi nhận được một thông báo. Bây giờ, chúng ta sử dụng thời gian chờ là 100 mili giây, do đó , nếu không có thông báo nào được nhận trong một phần mười của một giây, hàm sẽ trả về NULL. Chúng ta sẽ sử dụng logic này để cập nhật UI - User Interface.

Lưu ý rằng thời gian chờ mong muốn phải được chỉ định là GstClockTime, do đó tính bằng nano giây. Khi đó các số thể hiện các đơn vị thời gian khác nhau phải được nhân với các hằng số GST_SECOND, GST_MSECOND, GST_USECOND hoặc GST_NSECOND. Điều này cũng làm cho code của bạn dễ đọc hơn.

Nếu chúng ta nhận được một thông báo, chúng ta sẽ xử lý nó trong hàm handle_message, nếu không thì:

User interface refreshing

if (data.playing){

Nếu pipeline đang ở trạng thái PLAYING, đây là thời điểm để làm mới màn hình. Chúng ta không muốn làm bất cứ điều gì nếu chúng ta đang không ở trạng thái PLAYING, vì hầu hết các truy vấn sẽ thất bại.

Khổng 10 lần / giây, tốc độ làm mới đủ tốt cho giao diện người dùng. Chúng ta sẽ in trên màn hình vị trí mdei hiện tại, chúng ta có thể tìm hiểu vị trí này bằng cách truy vấn pipeline. Điều này liên quan đến một số bước sẽ được trình bày trong phần sau, nhưng vị trị và thời lượng là các truy vấn đủ phổ biến nên GstElement cung cấp các lựa chọn có sẵn thay thế dễ dàng hơn.

if (!gst_element_query_position(data.playbin, GST_FORMAT_TIME, &current))
  {
      g_printerr("Could not query current position.\n");
  }

gst_element_query_position() ẩn phần quản lý đối tượng truy vấn và trực tiếp cung cấp kết quả cho chúng ta.

if (!GST_CLOCK_TIME_IS_VALID(data.duration))
  {
      if (!gst_element_query_duration(data.playbin, GST_FORMAT_TIME, &data.duration))
      {
          g_printerr("Could not query current duration.\n");
      }
  }

Bây giờ là thời điểm thích hợp để biết thời lượng của luồng, với một hàm GstElement hỗ trợ khác: gst_element_query_duration()

g_print("Position %" GST_TIME_FORMAT " / %" GST_TIME_FORMAT "\r",GST_TIME_ARGS(current), GST_TIME_ARGS(data.duration));

Lưu ý việc sử dụng hằng số GST_TIME_FORMATGST_TIME_ARGS để cung cấp biểu diễn thân thiện với người dùng về các thời gian trong GStreamer.

if (data.seek_enabled && !data.seek_done && current > 10 * GST_SECOND)
  {
      g_print("\nReached 10s, performing seek...\n");
      gst_element_seek_simple(data.playbin, GST_FORMAT_TIME,
                              GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, 30 * GST_SECOND);
      data.seek_done = TRUE;
  }

Bây giờ, chúng ta thực hiện tìm kiếm, đơn giản bằng việc gọi gst_element_seek_simple trên pipeline. Rất nhiều điều phức tạp của việc tìm kiếm bị ẩn trong method này.

Hãy xem qua các tham số :

GST_FORMAT_TIME chỉ ra rằng chúng ta đang chỉ định điểm đến theo đơn vị thời gian. Các định dạng tìm kiếm khác nhau sử dụng các đơn vị khác nhau.

Sau đó đến với GstSeekFlags:

GST_SEEK_FLAG_FLUSH: thao tác này loại bỏ tất cả dữ liệu hiện có trong pipeline trước khi thực hiện tìm kiếm. Có thể tạm dừng một chút trong khi pipeline được nạp lại và dữ liệu mới sẽ bắt đầu hiển thị, nhưng làm tăng đáng kể “responsiveness” của ứng dụng. Nếu cờ này không được cung cấp, dữ liệu “stale - cũ” có thể được hiển thị trong một thời gian cho đến khi vị trí mới xuất hiện ở cuối pipeline.

GST_SEEK_FLAG_KEY_UNIT: Bới hầu hết các luồng video được mã hóa, không thể tìm kiếm các vị trí tùy ý mà chỉ tìm kiếm các khung hình tùy ý mà chỉ tìm kiếm các khung hình nhất định được gọi là Key Frames. Khi cờ này được sử dụng, tìm kiếm sẽ thực sự di chuyển bên trong đến Key Frame gần nhất và bắt đầu tạo dữ liệu ngay lập tức. Nếu cờ ngày không được sử dụng, pipeline sẽ di chuyển bên trong đến Key Frame gần nhất (nó không còn lựa chọn nào khác thay thế) nhưng dữ liệu sẽ không được hiển thị cho đến khi đạt được yêu cầu vị trí. Giải pháp thay thế cuối cùng này chính xác hơn nhưng có thể mất nhiều thời gian hơn.

GST_SEEK_FLAG_ACCURATE: Một số clip phương tiện không cung cấp đủ các mục thông tin, có nghĩa là tìm kiếm các vị trí tùy ý là mất thời gian. Trong những trường hợp này, GStreamer thường ước tính vị trí để tìm kiếm và thường hoạt động tốt. Nếu độ chính xác không đủ tốt cho trường hợp của bạn (bạn thấy tìm kiếm không đi đến thời gian chính xác mà bạn yêu cầu), sau đố cung cấp cờ này. Cảnh báo rằng bạn có thể mất nhiều thời gian hơn để tính toán vị trí tìm kiếm (rất lâu, trên một số tệp).

Cuối cùng, chúng tôi cung cấp vị trị để tìm kiếm. Kể từ khi chung tôi yêu cầu GST_FORMAT_TIME, giá tị phải được tính bằng nano giây nên ta thể hiện thời gian bằng giây, để đơn giản và sau đó nhân với GST_SECOND.

Messaage Pump

Hàm handle_message xử lý tất cả các thông báo nhận được thông qua bus của pipeline. Việc xử lý ERROREOS cũng giống như các hướng dẫn trước đây, vì vậy chúng ta chuyển sang phần thú vị:

case GST_MESSAGE_DURATION:
    data->duration = GST_CLOCK_TIME_NONE;
    break;

Thông báo được đăng trên bus bất cứ khi nào thời lượng của luồng thay đổi. Ở đây, chúng ta chỉ đơn giản đánh dấu thời lượng hiện tại là không có giá trị, vì vậy nó được truy vấn lại sau.

case GST_MESSAGE_ERROR:
    gst_message_parse_error(msg, &err, &debug_info);
    g_printerr("Error received from element %s: %s\n", GST_OBJECT_NAME(msg->src), err->message);
    g_printerr("Debugging information: %s\n", debug_info ? debug_info : "none");
    g_clear_error(&err);
    g_free(debug_info);
    data->terminate = TRUE;
    break;

Tìm kiếm và truy vấn thời gian thường chỉ nhận được câu trả lời khi ở trạng thái PLAYING hoặc PAUSED, vì tất cả các phần tử đã có cơ hội để nhận thông tin và tự cấu hình. Ở đây, chúng ta sử dụng biến playing để theo dõi pipeline có ở trạng thái PLAYING hay khộng. Ngoài ra, nếu chúng ta vừa nhập trạng thái PLAYING, chúng ta thực hiện truy vấn đầu tiên của mình. Chúng ta yêu cầu pipeline có cho phép tìm kiếm trên luồng hay không:

if (data->playing){
    GstQuery *query;
    gint64 start, end;
    query = gst_query_new_seeking(GST_FORMAT_TIME);
    if (gst_element_query(data->playbin, query))
    {
        gst_query_parse_seeking(query, NULL, &data->seek_enabled, &start, &end);
        if (data->seek_enabled)
        {
            g_print("Seeking is ENABLED from %" GST_TIME_FORMAT " to %" GST_TIME_FORMAT "\n",
                    GST_TIME_ARGS(start), GST_TIME_ARGS(end));
        }
        else
        {
            g_print("Seeking is DISABLED for this stream.\n");
        }
    }
    else
    {
        g_printerr("Seeking query failed.");
    }
    gst_query_unref(query);
}

gst_query_new_seeking() tạo một đối tượng truy vấn mới thuộc loại “đang tìm kiếm - seeking”, với định dạng GST_FORMAT_TIME. Điều này chỉ ra rằng chúng ta quan tâm đến việc tìm kiếm bằng cách chỉ định thời gian mới mà chúng ta muốn chuyển đến. Chúng ta cũng có thể yêu cầu GST_FORMAT_BYTES, sau đó tìm kiếm một vị trí byte cụ thể bên trong file nguồn, nhưng điều này thường kém hữu ích hơn.

Sau đó, đối tượng truy vấn này được chuyển đến pipeline bằng gst_element_query(). Kết quả được lưu trữ trong cùng một truy vấn, bạn có thể dễ dàng truy xuất bằng gst_query_parse_seeking(). Nó trích xuất một giá trị boolean cho biết liệu việc tìm kiếm có được phép hay không và phạm vi có thể tìm kiếm.

Đừng quên giải phóng đối tượng truy vấn khi bạn hoàn thành nó.

Và thế là xong! với kiến thức này, một trình phát đa phương tiên - media player có thể được xây dựng để cập nhật định kỳ thanh trượt dựa trên vị trí luồng hiện tại và cho phép tìm kiếm bằng cách di chuyển thanh trượt.

Conclusion

Trong bài hướng dẫn này trình bày:

  • Cách để truy vấn pipeline cho thông tin sử dụng GstQuery
  • Cách để lấy được các thông tin phổ biến như vị trí và thời lượng sử dụng gst_element_query_position()gst_element_query_duration()
  • Cách để tìm kiếm một vị trí bất kỳ trong luồng sử dụng gst_element_seek_simple()
  • trạng thái nào tất cả các hoạt động này có thể thực hiện

References