PyAV-Org / PyAV

Pythonic bindings for FFmpeg's libraries.
https://pyav.basswood-io.com/
BSD 3-Clause "New" or "Revised" License
2.53k stars 366 forks source link

Segfault when trying to use h264_v4l2m2m encoder on Raspberry Pi 4 with 64-bit OS #798

Closed samiamlabs closed 1 year ago

samiamlabs commented 3 years ago

Overview

Hi!

I'm trying to stream video with decent resolution and framerate over WebRTC (aiortc) with my Raspberry Pi 4. This pull request adding support for h264_omx seemed really promising at first: https://github.com/aiortc/aiortc/pull/488 Unfortunately, it seems that h264_omx is considered deprecated and won't be supported on arm64. (At the very least, I can't get it to work...)

I tried to just swap the codec name in the code related to the pull request to aiortc I mentioned above from "h264_omx" to "h264_v4l2m2m". The encoder context seems to be created without any exceptions (some warnings get printed though) but segfaults when trying to run self.codec.encode(frame).

Should it be possible to use the "h264_v4l2m2m" codec or is it simply not supported at this point?

Expected behavior

Well... I was hoping that h264_v4l2m2m would work as a drop-in replacement for h264_omx and allow me to stream video to remote control a robot using a Raspberry Pi 4.

Actual behavior

Hard to debug segfault which seems to happen outside the python code

Traceback:

gdb -ex r --args python3 webcam.py 
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "aarch64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from python3...
(No debugging symbols found in python3)
Starting program: /usr/bin/python3 webcam.py
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1".
[Detaching after fork from child process 1707]
======== Running on http://0.0.0.0:8080 ========
(Press CTRL+C to quit)
INFO:aiohttp.access:127.0.0.1 [06/Jul/2021:21:19:35 +0000] "GET / HTTP/1.1" 200 979 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
INFO:aiohttp.access:127.0.0.1 [06/Jul/2021:21:19:35 +0000] "GET /client.js HTTP/1.1" 200 2426 "http://localhost:8080/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
[New Thread 0xffffea2901e0 (LWP 1708)]
INFO:aioice.ice:Connection(0) Check CandidatePair(('172.19.0.2', 47684) -> ('192.168.1.60', 45032)) State.FROZEN -> State.WAITING
INFO:aiohttp.access:127.0.0.1 [06/Jul/2021:21:19:36 +0000] "POST /offer HTTP/1.1" 200 2557 "http://localhost:8080/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
Connection state is connecting
INFO:aioice.ice:Connection(0) Check CandidatePair(('172.19.0.2', 47684) -> ('192.168.1.60', 45032)) State.WAITING -> State.IN_PROGRESS
INFO:aioice.ice:Connection(0) Check CandidatePair(('172.19.0.2', 47684) -> ('192.168.1.60', 45032)) State.IN_PROGRESS -> State.SUCCEEDED
INFO:aioice.ice:Connection(0) ICE completed
Connection state is connected
[New Thread 0xffffe9a161e0 (LWP 1709)]
--------- Creating h264 encoder ----------
[h264_v4l2m2m @ 0xffffe4002550] driver 'bcm2835-isp' on card 'bcm2835-isp'
[h264_v4l2m2m @ 0xffffe4002550] driver 'bcm2835-isp' on card 'bcm2835-isp'
[h264_v4l2m2m @ 0xffffe4002550] driver 'bcm2835-isp' on card 'bcm2835-isp'
[h264_v4l2m2m @ 0xffffe4002550] driver 'bcm2835-isp' on card 'bcm2835-isp'
[h264_v4l2m2m @ 0xffffe4002550] driver 'bcm2835-codec' on card 'bcm2835-codec-isp'
[h264_v4l2m2m @ 0xffffe4002550] driver 'bcm2835-codec' on card 'bcm2835-codec-encode'
[h264_v4l2m2m @ 0xffffe4002550] Using device /dev/video11
[h264_v4l2m2m @ 0xffffe4002550] driver 'bcm2835-codec' on card 'bcm2835-codec-encode'
[h264_v4l2m2m @ 0xffffe4002550] Failed to set number of B-frames
[h264_v4l2m2m @ 0xffffe4002550] Failed to set number of B-frames
[h264_v4l2m2m @ 0xffffe4002550] Failed to set gop size
[h264_v4l2m2m @ 0xffffe4002550] h264 profile not found
[h264_v4l2m2m @ 0xffffe4002550] Encoder adjusted: qmin (0), qmax (51)
[h264_v4l2m2m @ 0xffffe4002550] Failed to set minimum video quantizer scale
[h264_v4l2m2m @ 0xffffe4002550] Failed to set maximum video quantizer scale
--Type <RET> for more, q to quit, c to continue without paging--

Thread 2 "python3" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0xffffea2901e0 (LWP 1708)]
0x0000fffff5a34440 in ?? () from /lib/aarch64-linux-gnu/libavcodec.so.58
(gdb) bt
#0  0x0000fffff5a34440 in  () at /lib/aarch64-linux-gnu/libavcodec.so.58
#1  0x0000fffff5a35f54 in  () at /lib/aarch64-linux-gnu/libavcodec.so.58
#2  0x0000fffff562cba0 in avcodec_send_frame () at /lib/aarch64-linux-gnu/libavcodec.so.58
#3  0x0000ffffec058f34 in __pyx_f_2av_5codec_7context_12CodecContext__send_frame_and_recv (__pyx_v_self=0xffffe9a1c430, __pyx_v_frame=0xffffe9a24340)
    at src/av/codec/context.c:5334
#4  0x0000ffffec06247c in __pyx_f_2av_5codec_7context_12CodecContext_encode
    (__pyx_v_self=0xffffe9a1c430, __pyx_skip_dispatch=<optimized out>, __pyx_optional_args=<optimized out>) at src/av/codec/context.c:6484
#5  0x0000ffffec0583f8 in __pyx_pf_2av_5codec_7context_12CodecContext_22encode (__pyx_v_frame=<optimized out>, __pyx_v_self=0xffffe9a1c430)
    at src/av/codec/context.c:6685
#6  __pyx_pw_2av_5codec_7context_12CodecContext_23encode (__pyx_v_self=0xffffe9a1c430, __pyx_args=<optimized out>, __pyx_kwds=<optimized out>)
    at src/av/codec/context.c:6662
#7  0x000000000048b7c4 in  ()
#8  0x00000000004fccac in _PyEval_EvalFrameDefault ()
#9  0x000000000047a648 in  ()
#10 0x000000000055dd8c in  ()
#11 0x00000000004fcb00 in _PyEval_EvalFrameDefault ()
#12 0x0000000000493328 in  ()
#13 0x00000000005011d4 in _PyEval_EvalFrameDefault ()
#14 0x00000000004f9dd4 in _PyEval_EvalCodeWithName ()
#15 0x00000000005952dc in _PyFunction_Vectorcall ()
#16 0x0000000000493408 in  ()
#17 0x00000000005916e4 in PyObject_Call ()
#18 0x00000000004fe054 in _PyEval_EvalFrameDefault ()
#19 0x00000000005950e8 in _PyFunction_Vectorcall ()
#20 0x00000000004fccac in _PyEval_EvalFrameDefault ()
#21 0x00000000005950e8 in _PyFunction_Vectorcall ()
#22 0x00000000005916e4 in PyObject_Call ()
#23 0x00000000004fe054 in _PyEval_EvalFrameDefault ()
--Type <RET> for more, q to quit, c to continue without paging--
#24 0x00000000005950e8 in _PyFunction_Vectorcall ()
#25 0x00000000004fccac in _PyEval_EvalFrameDefault ()
#26 0x00000000005950e8 in _PyFunction_Vectorcall ()
#27 0x00000000004fccac in _PyEval_EvalFrameDefault ()
#28 0x00000000005950e8 in _PyFunction_Vectorcall ()
#29 0x0000000000493504 in  ()
#30 0x00000000005916e4 in PyObject_Call ()
#31 0x00000000006100dc in  ()
#32 0x00000000006403ec in  ()
#33 0x0000fffff7e234fc in start_thread (arg=0xffffffffcbff) at pthread_create.c:477
#34 0x0000fffff7f1f67c in thread_start () at ../sysdeps/unix/sysv/linux/aarch64/clone.S:78

Investigation

Python debugging in VSCode and GDB

Reproduction

Run the webcam.py example in aiortc (modified to select h264 using the approach from this issue: https://github.com/aiortc/aiortc/issues/502) on Ubuntu 20.04.2 LTS for arm64 on a Raspberry Pi 4. Also, change the codec name from "h264_omx" to "h264_v4l2m2m" in https://github.com/aiortc/aiortc/blob/main/src/aiortc/codecs/h264.py Got an error related to the "profile" option after that so changed it to "profile:v" based on https://video.stackexchange.com/questions/2634/how-do-i-specify-baseline-h264-profile-using-ffmbc

I used a source build of FFmpeg 4.4 to get rid of the green overlay issue in 4.2.4 that is distributed by apt on focal (explained more here: https://www.willusher.io/general/2020/11/15/hw-accel-encoding-rpi4) but I don't know if that is relevant for this issue.

I can provide a Dockerfile for building FFmpeg etc if someone wants it.

Versions

Research

I have done the following:

samiamlabs commented 3 years ago

I tried to create a minimal example to reproduce this without using aiortc and ended up with this:

import av
from av import Codec
import os
import fractions

DEFAULT_BITRATE = 1000000  # 1 Mbps
MAX_FRAME_RATE = 30

test_video = os.path.join(os.path.dirname(
    os.path.realpath(__file__)), 'video.mp4')

container = av.open(test_video)
video_stream = next(s for s in container.streams if s.type == 'video')

codec = None

frame_count = 0

for packet in container.demux(video_stream):
    for frame in packet.decode():

        if codec is None:
            codec = av.CodecContext.create("h264_v4l2m2m", "w")
            codec.width = frame.width
            codec.height = frame.height
            codec.bit_rate = DEFAULT_BITRATE
            codec.pix_fmt = "yuv420p"
            codec.framerate = fractions.Fraction(MAX_FRAME_RATE, 1)
            codec.time_base = fractions.Fraction(1, MAX_FRAME_RATE)
            codec.options = {
                "profile:v": "baseline",
                "level": "31",
                "tune": "zerolatency",  # does nothing using h264_omx
            }

        print("frame count:", frame_count)
        frame_count += 1
        for package in codec.encode(frame):
            pass

print('finished')

This ran just fine on the Raspberry Pi 4. No segfault. Maybe I should figure out how to write the encoded video to a file, just to make sure it actually worked but I suspect it did.

I already verified that this works: ffmpeg -i video.mp4 -c:v h264_v4l2m2m -b:v 8M -c:a copy test.mp4

Am I correct in assuming that this means the problem is probably in aiortc and not in PyAV?

In that case I should probably close this issue...

jlaine commented 3 years ago

Honestly these issues just depress me, maintaining opensource projects is no fun.

samiamlabs commented 3 years ago

Haha, yea I hear you @jlaine...

I spent way too much time over the weekend trying to figure out what's causing this specific issue is really starting to annoy me.

Thought for quite some time that that the reason was that h264_v4l2m2m only could handle inputs that have yuv420p as their pixel format. (my logitech c922 outputs yuyv422). It just doesn't seem like there are that many other differences between the frames that work and those that segfault.

Webcam Frame:

<av.VideoFrame #0, pts=0 yuyv422 640x480 at 0xffff6f0c12e0>
dts:91271867741
format:<av.VideoFormat yuyv422, 640x480>
index:0
interlaced_frame:0
is_corrupt:False
key_frame:1
pict_type:<av.video.frame.PictureType:I(0x1)>
planes:(<av.VideoPlane 61440...f6d6f2860>,)
pts:0
side_data:<av.sidedata.sidedata.SideDataContainer object at 0xffff6d6f2a40>
time:0.0
time_base:Fraction(1, 1000000)
height:480
width:640

File Frame:

<av.VideoFrame #0, pts=0 yuv420p 1920x1080 at 0xffffa0e000a0>
dts:0
format:<av.VideoFormat yuv420p, 1920x1080>
index:0
interlaced_frame:0
is_corrupt:False
key_frame:1
pict_type:<av.video.frame.PictureType:I(0x1)>
planes:(<av.VideoPlane 20736...f780279a0>, <av.VideoPlane 51840...f78027b80>, <av.VideoPlane 51840...f78027a40>)
pts:0
side_data:<av.sidedata.sidedata.SideDataContainer object at 0xffff78027db0>
time:0.0
time_base:Fraction(1, 12800)
height:1080
width:1920

I wrote a bunch of test with different input frames, and I can't figure out a pattern that explains why some of them segfault.

import os
import av
from av import VideoFrame
import numpy as np
import fractions
import cv2 as cv
import faulthandler

DEFAULT_BITRATE = 1000000  # 1 Mbps
MAX_FRAME_RATE = 30

test_video = os.path.join(os.path.dirname(
    os.path.realpath(__file__)), 'video.mp4')

def create_encoder_context(codec_name, frame):
    print('creating encoder of type: ' + codec_name)
    codec = av.CodecContext.create(codec_name, "w")
    codec.width = frame.width
    codec.height = frame.height
    codec.bit_rate = DEFAULT_BITRATE
    codec.pix_fmt = "yuv420p"
    codec.framerate = fractions.Fraction(MAX_FRAME_RATE, 1)
    codec.time_base = fractions.Fraction(1, MAX_FRAME_RATE)
    codec.options = {
    }
    codec.open()
    return codec

def test_codec_libx264_opencv():
    cap = cv.VideoCapture(0)
    ret, opencv_frame = cap.read()

    frame = VideoFrame.from_ndarray(opencv_frame, format="bgr24")

    codec = create_encoder_context('libx264', frame)
    codec.encode(frame)

    cap.release()
    print('no segfault in test_codec_libx264_opencv')

def test_codec_h264_v4l2m2m_opencv():
    cap = cv.VideoCapture(0)
    ret, opencv_frame = cap.read()

    frame = VideoFrame.from_ndarray(opencv_frame, format="bgr24")

    codec = create_encoder_context('h264_v4l2m2m', frame)
    codec.encode(frame)

    cap.release()
    print('no segfault in test_codec_h264_v4l2m2m_opencv')

def test_codec_h264_v4l2m2m_opencv_yuv420p():
    cap = cv.VideoCapture(0)
    ret, opencv_frame = cap.read()

    yuv_img = cv.cvtColor(opencv_frame, cv.COLOR_BGR2YUV_I420)
    frame = VideoFrame.from_ndarray(yuv_img, format="yuv420p")
    # frame.pict_type = 'I'

    codec = create_encoder_context('h264_v4l2m2m', frame)
    codec.encode(frame)

    cap.release()
    print('no segfault in test_codec_h264_v4l2m2m_opencv_yuv420p')

def test_codec_libx264_opencv_yuv420p():
    cap = cv.VideoCapture(0)
    ret, opencv_frame = cap.read()

    yuv_img = cv.cvtColor(opencv_frame, cv.COLOR_BGR2YUV_I420)
    frame = VideoFrame.from_ndarray(yuv_img, format="yuv420p")
    # frame.pict_type = 'I'

    codec = create_encoder_context('libx264', frame)
    codec.encode(frame)

    cap.release()
    print('no segfault in test_codec_libx264_opencv_yuv420p')

def test_codec_h264_v4l2m2m_file():
    container = av.open(test_video)
    video_stream = next(s for s in container.streams if s.type == 'video')

    for packet in container.demux(video_stream):
        for frame in packet.decode():
            codec = create_encoder_context('h264_v4l2m2m', frame)
            codec.encode(frame)
            break
        break

    container.close()
    print('no segfault in test_codec_h264_v4l2m2m_file')

def test_codec_h264_v4l2m2m_webcam():
    container = av.open('/dev/video0')
    video_stream = next(s for s in container.streams if s.type == 'video')

    for packet in container.demux(video_stream):
        for frame in packet.decode():
            codec = create_encoder_context('h264_v4l2m2m', frame)
            codec.encode(frame)
            break
        break

    container.close()
    print('no segfault in test_codec_h264_v4l2m2m_webcam')

def test_codec_h264_libx264_webcam():
    container = av.open('/dev/video0')
    video_stream = next(s for s in container.streams if s.type == 'video')

    for packet in container.demux(video_stream):
        for frame in packet.decode():
            codec = create_encoder_context('libx264', frame)
            codec.encode(frame)
            break
        break

    container.close()
    print('no segfault in test_codec_h264_libx264_webcam')

# faulthandler.enable()

# works
test_codec_libx264_opencv()
test_codec_libx264_opencv_yuv420p()
test_codec_h264_v4l2m2m_file()
test_codec_h264_libx264_webcam()

# segfaults
# test_codec_h264_v4l2m2m_opencv()
# test_codec_h264_v4l2m2m_webcam()
# test_codec_h264_v4l2m2m_opencv_yuv420p()

I also thought for a good while that the problem had to be outside PyAv, but it turned out that ffmpeg -i /dev/video0 -codec h264_v4l2m2m -pix_fmt yuv420p test.h264 actually works, so I'm not so sure about that anymore.

Pretty close to giving up on solving this, but it's possible that I'm close to a solution also... I don't know, this kind of sucks....

jlaine commented 3 years ago

OK you've obviously spent a lot of time on this, thanks so much for looking for a way to reproduce it!

I'm a bit bogged down at the moment, I hope to have time over the summer to pull out my Rpi4 and experiment with it.

samiamlabs commented 3 years ago

Ok, sounds great!

I may end up switching to a jetson tx2 and trying my luck at getting the nvenc encoder to work with aiortc instead for the time being :)

Just to be clear, I really don't want to imply that you have some sort of obligation to fix this and you seem to be doing a really good job maintaining these projects as far as I can tell :)

github-actions[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

jlaine commented 2 years ago

Still hoping to get round to this eventually.

Pieter-Antonio commented 2 years ago

I stumbled upon this issue myself and have some different results, this was all run on a Raspberry Pi 3B+ with Raspberry Pi OS 64 bit, using FFmpeg from the repositories.

There is OpenCV image processing code running on a process which gets its frames from the webcam. I then write these frames to a v4l2loopback device in the YUV420p format.

A separate process then streams this loopback device as if it were a webcam stream by WebRTC with aiortc (using the v4l2m2m encoder). I firstly also got the segfaults, but managed to get it working by outputting YUV420P frames from OpenCV instead of BGR3 and setting the pixelformat option in the MediaPlayer from aiortc accordingly to YUV420P.

I also got an error that with v4l2m2m the "profile" option in aiortc/codecs/h264.py "create_encoder_context" didn't exist, this had to be changed to "profile:v".

So according to me the error with @samiamlabs webcam example is the unsupported pixel format and with the opencv example, that the encoder is generated in the "create_encoder_context" with YUV420P, but OpenCV outputs BGR3. I honestly have no idea why your opencv_yuv420p example segfaults.

github-actions[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

unitree-czk commented 2 years ago

Any Luck?

github-actions[bot] commented 1 year ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.