Trong phần 1, mình đã trình bày lý thuyết về các thành phần cơ bản trong một GStreamer pipeline rồi, giờ thì bắt tay vào phần thực hành nào 😀
À vì các bạn đã tìm hiểu đến GStreamer, nên mình mặc định bạn đã có kiến thức cơ bản sau nhé ✌️, chứ hướng dẫn từng bước 1 thì 4-5 series cũng không đủ 🥲:
- WSL, các lệnh Linux CLI cơ bản như để tải package, chạy file
- Docker, cách tạo image, chạy container, dùng Docker Compose, cách mount, attach vào container để chạy code.
- Python cơ bản: tải, sử dụng thư viện, OOP với Python
Và để dễ cài đặt, mình xin phép giới hạn phạm vi bài viết trên Linux thôi nhé, bạn nào dùng Window thì có thể sử dụng WSL, còn ngôn ngữ lập trình chính thì mình sẽ dùng Python. Giờ thì bắt đầu nào.
1. Cài đặt GStreamer
1.1. Cài đặt GStreamer package
Để khỏi mắc công cập nhật phần này khi GStreamer có update mới, nên mình xin dẫn thẳng link tới bài hướng dẫn gốc nhé 😀. Bạn có thể tìm thấy hướng dẫn cài đặt GStreamer trên trang document chính thức của GStreamer. Ví dụ như để cài GStreamer cho Linux, bạn có thể làm theo hướng dẫn tại đây: https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c
1.2. Binding GStreamer sang Python
Vì GStreamer được viết bằng C, nên để có thể thao tác với GStreamer bằng câu lệnh Python, ta cần phải binding các class, hàm, hằng, … của GStreamer từ C sang Python. Và việc binding này được thực hiện thông qua PyGObject. Nên khi code, thỉnh thoảng bạn sẽ gặp các dòng import sau để sử dụng GStreamer:
import gi
gi.require_version("Gst", "1.0")
from gi.repository import Gst
Với gi là viết tắt cho GObject Introspection (thuộc PyGObject) và Gst là viết tắt cho GStreamer
Bạn có thể xem hướng dẫn cài PyGObject tại đây nhé: https://stackoverflow.com/a/62533271.
1.3. Cài đặt môi trường bằng Docker (thay cho hai bước trên)
Hoặc bạn có thể sử dụng Docker để tạo image tự cài đặt Python, GStreamer, PyGObject. Mình có viết sẵn Dockerfile
và docker-compose.yaml
tại repo này để bạn có thể build, run, và attach vào dùng ngay.
2. Cách chạy GStreamer pipeline
Trước tiên bạn cần kiểm tra đã cài GStreamer thành công hay chưa, bằng cách chạy lệnh sau trên CLI:
gst-launch-1.0 --version
Lúc này terminal sẽ trả về version của GStreamer, và có thể hiện thị một vài Warning vô hại (maybe 😀)
Trong bài này mình sẽ trình bày hai cách để chạy 1 GStreamer pipeline, đó là chạy bằng GStreamer CLI, và chạy bằng Python code.
2.1. Chạy GStreamer CLI
Đây là cách đơn giản và nhanh nhất để kiểm tra pipeline của bạn được tạo thành công hay không, trước khi chuyển sang Python để thêm các thao tác phức tạp.
Đầu tiên bạn cần tải video video_h264.mp4 để test. Bạn cũng có thể dùng video khác, nếu đảm bảo codec của video đó là h264, nếu không có thể gặp lỗi ngoài ý muốn.
Sau đây là một pipeline dùng để lưu một video y chang video input (?? 😀 ?? ờ thì trông hơi vô nghĩa, nhưng mình chỉ muốn demo nhanh và đảm bảo pipeline này chạy được trên cả server không hỗ trợ giao diện, nên mình sẽ không làm mấy cái hiển thị cửa sổ như trong Tutorial đầu của GStreamer đâu):
gst-launch-1.0 filesrc location=video_h264.mp4 ! filesink location=output.mp4
Đợi một khoảng thời gian, bạn sẽ thấy file output.mp4 y hệt như video_h264.mp4 🥳.
GStreamer CLI chạy pipeline sẽ bắt đầu với gst-launch-1.0
, sau đó là các element được cách nhau bởi dấu !. Bên cạnh element filesrc
và filesink
có các cụm như location=...
trước dấu !, lúc này location=...
sẽ là property của element filesrc
và filesink
, dùng để set đường dẫn của file input và output.
2.2. Chạy GStreamer với Python
Ờ thì cũng có hai cách chạy GStreamer với Python 😀. Mình bắt đầu với cách gọn gàng nhất nhé:
Chạy bằng Gst.parse_launch
import gi
gi.require_version("Gst", "1.0")
from gi.repository import Gst def main(): Gst.init(None) pipeline = Gst.parse_launch("filesrc location=video_h264.mp4 ! filesink location=output.mp4") pipeline.set_state(Gst.State.PLAYING) bus = pipeline.get_bus() bus.timed_pop_filtered(Gst.CLOCK_TIME_NONE, Gst.MessageType.ERROR | Gst.MessageType.EOS) pipeline.set_state(Gst.State.NULL) print("Pipeline stopped") if __name__ == "__main__": main()
- Đầu tiên,
Gst.init(None)
sẽ làm nhiệm vụ setup môi trường trước khi chạy pipeline GStreamer - Sau đó, chúng ta khởi tạo pipeline bằng
Gst.parse_launch
, và string truyền vào sẽ tương tự như lệnh GStreamer CLI, thiếu mỗigst-launch-1.0
. - Ta chạy pipeline bằng cách state pipeline sang
PLAYING
. Lúc này pipeline sẽ chạy trong một thread khác. - Bởi vì pipeline chạy trong một thread khác, và không chặn main thread của python. Nên chương trình sẽ tiếp tục chạy câu lệnh tiếp theo. Do đó chúng ta cần ngăn việc chương trình bị thoát do đã chạy xong hết lệnh, trong khi pipeline chưa chạy xong. Ta có thể ngăn này bằng cách dùng bus để chờ pipeline gửi các thông báo như kết thúc (
EOS
) hoặc bị lỗi (ERROR
). - Sau cùng, ta giải phóng tài nguyên sử dụng bằng cách set state của pipeline sang
NULL
.
Chạy bằng cách make, link, add, play thủ công
import gi
gi.require_version("Gst", "1.0")
from gi.repository import Gst def main(): Gst.init(None) pipeline = Gst.Pipeline.new("simple-pipeline") src = Gst.ElementFactory.make("filesrc") sink = Gst.ElementFactory.make("filesink") src.set_property("location", "video_h264.mp4") sink.set_property("location", "output.mp4") pipeline.add(src) pipeline.add(sink) src.link(sink) pipeline.set_state(Gst.State.PLAYING) bus = pipeline.get_bus() bus.timed_pop_filtered( Gst.CLOCK_TIME_NONE, Gst.MessageType.ERROR | Gst.MessageType.EOS ) pipeline.set_state(Gst.State.NULL) print("Pipeline stopped") if __name__ == "__main__": main()
- Sau khi chạy
Gst.init(None)
, ta khởi tạo pipeline để bắt đầu thêm từng element vào bằngGst.Pipeline.new
. - Sau đó ta tạo hai element source và sink bằng
Gst.ElementFactory.make
- Set
location
từng element bằngset_property
- Vì các element này hiện đang tự do, nên ta cần phải đưa chúng vào trong pipeline thông qua
add
và link lại với nhau bằnglink
. - Cuối cùng ta chạy pipeline, chờ pipeline, và set pipeline về
NULL
như cách trên.
Cơ bản thì hai phương pháp này cùng cho ra một kết quả. Nhưng với phương pháp tạo thủ công, bạn có thể thao tác với các element liền sau khi khởi tạo. Còn cách dùng parse_launch
, bạn phải đặt tên cho chúng bằng cách set property name=element
, rồi truy xuất bằng pipeline.get_by_name("element")
.
Để xem nào, chúng ta đã biết cách chạy pipeline rồi này, hình dung được cách khởi tạo và kết nối các element như thế nào rồi này. Và để đi xa hơn, chúng ta cần phải biết cách debug pipeline (không phải copy lỗi rồi paste lên ChatGPT đâu 😀)
3. Các cách debug Gstreamer pipeline
Trước khi debug, chúng ta phải có bug 😈. Lần này cùng xem xét pipeline cùng chức năng nhưng phức tạp hơn:
import gi
gi.require_version("Gst", "1.0")
from gi.repository import Gst def main(): Gst.init(None) pipeline = Gst.parse_launch( "filesrc location=video_h264.mp4 ! " "qtdemux ! " "mpeg4videoparse ! " "qtmux ! " "filesink location=output.mp4" ) pipeline.set_state(Gst.State.PLAYING) bus = pipeline.get_bus() bus.timed_pop_filtered(Gst.CLOCK_TIME_NONE, Gst.MessageType.ERROR | Gst.MessageType.EOS) pipeline.set_state(Gst.State.NULL) print("Pipeline stopped") if __name__ == "__main__": main()
Mình giải thích sơ về luồng data sẽ qua từng element trông như thế nào nhé:
- filesrc: đọc data từ file video_h264.mp4, sau đó đưa về byte stream cho element tiếp theo
- qtdemux: từ byte stream, tách video stream đã được encode để gửi cho element tiếp theo
- mpeg4videoparse: từ video stream đã được encode, parse stream để đảm bảo stream đầu ra sẵn sàng để mux (đóng gói vào container)
- qtmux: từ video stream đầu vào, đóng gói thành container (như .mp4) để sẵn sàng lưu thành file
- filesink: lưu data nhận được vào file output.mp4
Khi chạy đoạn code trên, ta vẫn nhận được dòng print "Pipeline stopped"
và file output.mp4, trông có vẻ bình thường chán. Nhưng file output.mp4 lại có kích thước là 0 byte và không thể bật lên được. Đây là lúc chúng ta cần debug pipeline để xem bị lỗi ở đâu.
3.1. Chỉnh gstreamer debug log level
Thông thường, GStreamer sẽ log ra các thông tin trong quá trình chạy pipeline, các dòng log này có thể dùng để in ra các hoạt động diễn ra của từng element, trong đó có thể bao gồm cả thông tin về lỗi. Dựa trên bài Tutorial 11 về debug log của GStreamer, GStreamer log có thể chia làm các cấp độ sau
- 0: none - không log thông tin debug gì cả.
- 1: ERROR - log thông tin về lỗi.
- 2: WARNING - log thông tin về các cảnh báo nguy cơ xảy ra lỗi trong pipeline.
- Ngoài ra còn các level như 3: FIXME, 4: INFO, 5: DEBUG, … mà bạn có thể tham khảo ở link trên.
Theo mặc định, GStreamer sẽ đặt log level là 0, nên sẽ không có gì hữu ích được in ra khi pipeline bị lỗi. Và mình có thể thay đổi level này bằng cách thay đổi biến môi trường GST_DEBUG
khi chạy:
import os
os.environ["GST_DEBUG"] = "2"
Ngoài ra, bạn còn có thể dùng hàm binding của GStreamer như:
Gst.debug_set_default_threshold(Gst.DebugLevel.WARNING)
Tuy nhiên, mình muốn dùng
os.environ
để nhấn mạnh là bạn có thể thay đổi log level thông qua biến môi trường, hơn là chỉ chạy bên trong file python
Mình chọn 2 vì mình thấy nó in vừa đủ thông tin để debug, và không quá nhiều để gây nhiễu nội dung (vì càng tăng GST_DEBUG
, số lượng log in ra sẽ càng nhiều).
Mình đặt đoạn code này trên đầu file python vừa rồi và chạy lại. Và mình nhận được khá nhiều dòng Warning, một số dòng cho thấy quá trình load thư viện, link giữa các element với nhau, đặc biệt là các dòng cuối có kèm theo chữ error
trước khi in "Pipeline stopped"
:
WARN qtdemux qtdemux.c:6948:gst_qtdemux_loop:<qtdemux0> error: Internal data stream error.
WARN qtdemux qtdemux.c:6948:gst_qtdemux_loop:<qtdemux0> error: streaming stopped, reason not-linked (-1)
Theo đọc hiểu, mình có thể thấy element qtdemux đang gặp lỗi khi link element. Nhưng hiện đang có hai element kết nối với qtdemux filesrc và mpeg4videoparse, để biết việc link đang gặp vấn đề ở đâu, mình có thể in ảnh sơ đồ kết nối của pipeline ra.
3.2. Lấy ảnh pipeline
GStreamer hỗ trợ xuất file sơ đồ kết nối với đuôi là .dot
. Và file này có thể chuyển sang ảnh bằng package GraphViz
. Nên trước tiên ta phải đảm bảo đã tải thư viện này về máy:
apt install graphviz
Sau đó ta thêm hàm này:
import subprocess def save_pipeline_graph(pipeline, png_file="pipeline.png"): dot_file = png_file.replace(".png", ".dot") with open(dot_file, "w") as f: f.write(Gst.debug_bin_to_dot_data(pipeline, Gst.DebugGraphDetails.ALL)) subprocess.run(["dot", "-Tpng", dot_file, "-o", png_file]) print(f"Saved pipeline graph as {png_file}")
và chạy nó sau khi đưa pipeline về state PLAYING
:
pipeline.set_state(Gst.State.PLAYING)
save_pipeline_graph(pipeline)
bus = pipeline.get_bus()
Hàm save_pipeline_graph
sẽ lưu thông tin graph vào pipeline.dot
, sau đó dùng GraphViz để convert sang file pipeline.png
như sau:
Như trên hình, ta có thể thấy các element sẽ được đại diện bằng các hình chữ nhật bo tròn, với source element màu đỏ, filter element màu xanh lục, và sink element màu xanh biển. Và hai element được link thành công với nhau được nối bằng dấu mũi tên. Và từ đây, ta đã có thể trả lời cho câu hỏi về log error phía trên: hiện qtdemux và mpeg4videoparse hiện tại không thể link được với nhau.
Thông thường, hai element khi không link được với nhau khi source pad của element trước không có caps tương thích với sink pad của element sau. Do đó chúng ta cần phải xem caps của qtdemux source pad và mpeg4videoparse sink pad hỗ trợ những định dạng nào.
3.3. Xem thông tin element bằng gst-inspect-1.0
gst-inspect-1.0
là một GStreamer CLI cho phép xem thông tin chi tiết của các element, bao gồm các loại pad, caps từng pad, signal và property của element. Ví dụ để xem thông tin về qtdemux, ta chạy lệnh:
gst-inspect-1.0 qtdemux
Vì hiện tại ta đang cần tìm caps cho source pad của qtdemux, ta có thể tạm bỏ qua các thông tin khác và kéo đến mục Pad Templates:
, ta sẽ thấy các dòng sau:
SRC template: 'video_%u' Availability: Sometimes Capabilities: ANY
Bởi vì qtdemux tách các stream ra khỏi container, nên sẽ có nhiều source tương ứng cho từng loại stream. Ở đây chúng ta đang làm việc với video nên chỉ cần quan tâm đến SRC template: 'video_%u'
thôi. Và Capabilities
ở đây đang là ANY
, tức là caps của qtmux source pad cho video sẽ không cố định, và có thể thay đổi phụ thuộc vào lúc chạy pipeline, cụ thể trong pipeline này là phụ thuộc vào file input mà chúng ta đặt vào filesrc.
Chúng ta tiếp tục kiểm tra với mpeg4videoparse:
gst-inspect-1.0 mpeg4videoparse
Và chúng ta cần tìm thông tin về caps cho sink pad của mpeg4videoparse, nên chúng ta lướt đến các dòng sau:
SINK template: 'sink' Availability: Always Capabilities: video/mpeg mpegversion: 4 systemstream: false video/x-divx divxversion: [ 4, 5 ]
Các dòng này cho thấy mpeg4videoparse sink pad mong đợi input có codec là mpeg
hoặc x-divx
. Và qtdemux thì source pad có caps phụ thuộc vào file input đâu vào như nói ở trên. Do đó bây giờ chúng ta cần phải kiểm tra file đầu vào. Cố lên, gần xong rồi 😊.
Nếu máy bạn có kết nối mạng và muốn xem thông tin về các element trực quan hơn, bạn có thể search trên google với tên của element + từ khóa GStreamer, bạn sẽ thấy các trang document của GStreamer về các element trên như:
- qtdemux: https://gstreamer.freedesktop.org/documentation/isomp4/qtdemux.html?gi-language=c
- mpeg4videoparse: https://gstreamer.freedesktop.org/documentation/videoparsersbad/mpeg4videoparse.html?gi-language=c
3.4. Kiểm tra codec của source
Có nhiều cách để kiểm tra video codec của một video, ta có thể sử dụng ffprobe
, hoặc gst-discoverer-1.0
. Để đúng chủ đề, mình sẽ sử dụng gst-discoverer-1.0
:
Nếu trên máy bạn vẫn chưa có
gst-discoverer-1.0
thì tải package sau nhéapt install gstreamer1.0-plugins-base-apps
gst-discoverer-1.0 video_h264.mp4
Câu lệnh sẽ show thông tin chi tiết về video, như thời lượng, width, height, FPS, và codec:
video #1: H.264 (High Profile)
Vì video đầu vào đang chạy với codec là H.264, trong khi chúng ta lại dùng mpeg4videoparse để parse data, vì khác định dạng mong đợi (caps) nên hai element qtdemux và mpeg4videoparse đã không thể link được với nhau. Nên để giải quyết bug này, chúng ta sẽ dùng h264parse để đúng codec của video.
3.5. Code đã được fix
Sau đây là toàn bộ code mà chúng ta đã thêm từ đầu đến giờ:
import os
os.environ["GST_DEBUG"] = "2"
import subprocess import gi
gi.require_version("Gst", "1.0")
from gi.repository import Gst def save_pipeline_graph(pipeline, png_file="pipeline.png"): dot_file = png_file.replace(".png", ".dot") with open(dot_file, "w") as f: f.write(Gst.debug_bin_to_dot_data(pipeline, Gst.DebugGraphDetails.ALL)) subprocess.run(["dot", "-Tpng", dot_file, "-o", png_file]) print(f"Saved pipeline graph as {png_file}") def main(): Gst.init(None) pipeline = Gst.parse_launch( "filesrc location=video_h264.mp4 ! " "qtdemux ! " "h264parse ! " "qtmux ! " "filesink location=output.mp4" ) pipeline.set_state(Gst.State.PLAYING) save_pipeline_graph(pipeline) bus = pipeline.get_bus() bus.timed_pop_filtered(Gst.CLOCK_TIME_NONE, Gst.MessageType.ERROR | Gst.MessageType.EOS) pipeline.set_state(Gst.State.NULL) print("Pipeline stopped") if __name__ == "__main__": main()
Khi chạy xong, chúng ta không còn thấy dòng warning reason not-linked (-1)
nữa. Và các element của pipeline đều đã được link với nhau:
Bam, video output.mp4 đã xem được, code lưu video thành video của chúng ta đã được chạy thành công.
4. Kết bài
Bài này hơi dài nhỉ :v, chúng ta vừa đi qua:
- Cách cài đặt GStreamer, binding GStreamer với Python (mặc dù mình toàn dẫn link khác vào =))) )
- Cách chạy GStreamer pipeline bằng GStreamer CLI và Python code
- Cách cài đặt log level cho GStreamer pipeline
- Cách xuất GStreamer pipeline thành hình ảnh
- Cách kiểm tra thông tin từng element của GStreamer bằng
gst-inspect-1.0
- Cách kiểm tra video source bằng
gst-discoverer-1.0
Ngoài ra còn một số vấn đề khác mình cũng muốn bàn tới như các loại pad trong element, cách theo dõi metadata lấy từ buffer, autoplugging, … nhưng hơi dài rồi nên mình hẹn bài sau nhé.