3

I need to process the video stream and the klvdata streams simultaneously in real-time in OpenCV/Python. I'm using FFMPEG to read the file or stream as OpenCV does not retain the klvdata. I pass the data to OpenCV with the subprocess module.

My problem is I cannot figure out how to map both the video and klvdata to the same subprocess pipe simultaneously?

My code:

#!/usr/bin/env python3
import sys, json, klvdata;
from subprocess import PIPE
import subprocess as sp
import cv2
import numpy

command = ['ffmpeg',
    '-i', 'DayFlight.mpg',
    '-map', '0:0',
    '-map', '0:d',        
    '-pix_fmt', 'bgr24',
    '-c:v', 'rawvideo',      
    '-an','-sn',              
    '-f', 'image2pipe', '-',
    '-c:d', 'copy',
    '-f','data',
    ]

pipe = sp.Popen(command, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE, bufsize=10**8)

while True:
   raw_image = pipe.stdout.read(1280*720*3)
   image =  numpy.fromstring(raw_image, dtype='uint8')
   image = image.reshape((720,1280,3))          
   if image is not None:
      cv2.imshow('Video', image)
   if cv2.waitKey(1) & 0xFF == ord('q'):
      break
   for packet in klvdata.StreamParser(pipe.stdout): 
      metadata = packet.MetadataList()
      print(metadata)
pipe.stdout.flush()
cv2.destroyAllWindows()

Produces the below error:

Traceback (most recent call last):
  File "test_cv.py", line 32, in <module>
    metadata = packet.MetadataList()
AttributeError: 'UnknownElement' object has no attribute 'MetadataList'

Any help is greatly appreciated.

  • Look [here](https://github.com/paretech/klvdata/blob/master/klvdata/streamparser.py#L48): You haven't [added](https://github.com/paretech/klvdata/blob/master/klvdata/streamparser.py#L54) any parsers. – Timus Mar 14 '22 at 08:25
  • @Timus, Thanks for your feedback. Sorry for the elementary question but how do I learn more about parsers? Are you aware of a sample I can use as a reference? – seabass1217 Mar 14 '22 at 08:49
  • Sorry, I don't know the package, I've just peeked into it's source code. – Timus Mar 14 '22 at 08:50
  • questions on the usage of that package should probably be addressed to the package's author first – Christoph Rackwitz Mar 14 '22 at 10:15
  • Is there a reason why metadata has to be output with the video stream? Why not separately `ffprobe` it? – kesh Mar 14 '22 at 13:21
  • @kesh, My thinking is to use the same command as to synchronize the data. Is it possible to sync data between ffprobe and ffmpeg? – seabass1217 Mar 14 '22 at 13:46
  • I see. So, what you want to do is to read simultaneously both video and data stream of `'DayFlight.mpg'`. Use of the term 'metadata' is confusing in the context of media files as it specifically means the media/track information added most often to the container/stream headers. (You may want to edit your post to clarify this point.) Anyway, `ffprobe` is no use in your case. – kesh Mar 14 '22 at 14:04
  • What is your OS? – kesh Mar 14 '22 at 14:05
  • Do you have any sample video with KLV data we can use? Does every frame has private KLV data that is synchronized with that frame (or do you need to synchronize by timestamps some how)? Do you have to parse it in "real-time style", or is it possible to split video/KLV to files and parse/synchronize in a second pass? – Rotem Mar 14 '22 at 17:49
  • @kesh, Good point. I will update my post to clarify. – seabass1217 Mar 15 '22 at 05:32
  • @Rotem, Yes, I have a sample video and I'm assuming every frame has private KLV data that is synchronized with the frame. I'm not sure how to verify klvdata by the frame. And yes, I have to parse in real-time. – seabass1217 Mar 15 '22 at 05:36
  • @Rotem, The sample file can be found: ```http://samples.ffmpeg.org/MPEG2/mpegts-klv/Day%20Flight.mpg``` – seabass1217 Mar 15 '22 at 05:47
  • [This post](https://www.aeronetworks.ca/2018/05/mpeg-2-transport-streams.html) describes how to extract the KLV data from "Day Flight.mpg" using FFmpeg. The output data file is only 977 bytes. There is no private KLV data with each frame (or FFmpeg extracts only small portion of the data). I also tried this command: `ffmpeg -y -i "Day Flight.mpg" -map 0:d -c copy -copy_unknown -f data raw.bin` – Rotem Mar 15 '22 at 10:22

1 Answers1

3

For splitting the video and data, we may map the video stream to stderr pipe and map the KLV data stream to to stdout pipe.

The same technique is used for separating video and audio in my following answer.

Accurate synchronization between the the video frame and the corresponding data is relatively simple when each video frame has private KLV data (synchronize by sequential order).

The Day Flight.mpg sample file has much fewer data packets than frames, and accurate synchronization is not possible using the suggested solution (I don't think it is possible using the pipes approach).
We may still apply some coarse synchronization - assume the data and the frame are read in time proximity.

Suggested way for splitting the video and the data:

                                            -----------
                                       --->| Raw Video | ---> stderr (pipe)
 -----------        -------------     |     -----------    
| Input     |      | FFmpeg      |    |
| Video with| ---> | sub-process | ---      
| Data      |      |             |    |    
 -----------        -------------     |     -----------
                                       --->| KLV data  | ---> stdout (pipe)
                                            -----------

The video and the data are read in two separate threads:

  • Video reader thread - read raw video frames (in BGR) format.
  • Data reader thread - read and parse KLV data.

According to Wikipedia, the KLV format is not well defined:

Keys can be 1, 2, 4, or 16 bytes in length.
Presumably in a separate specification document you would agree on a key length for a given application.

In the sample video, the key length is 16 bytes, but it is not guaranteed...


Reading the KLV data from stdout pipe:
When reading data from a pipe (in real-time like manner), we need to know the expected number of bytes to read.
That forces us to do partial parsing of the KLV data:

  • Read the "key" (assume 16 bytes length).
  • Read the "length" - there is some challenge with "BER length" standard.
  • Read the the "data" (size to read is defined by the length).

After reading the key, length and data, we have one "KLV data packet", we can send to the KLV data parser.


Here is a code sample that woks with Day Flight.mpg sample input file:

#!/usr/bin/env python3
import klvdata
import subprocess as sp
import shlex
import threading
import numpy as np
import cv2
from io import BytesIO

# Video reader thread.
def video_reader(pipe):
    cols, rows = 1280, 720  # Assume we know frame size is 1280x720

    counter = 0
    while True:
        raw_image = pipe.read(cols*rows*3)  # Read raw video frame

        # Break the loop when length is too small
        if len(raw_image) < cols*rows*3:
            break

        if (counter % 60) == 0:
            # Show video frame evey 60 frames
            image = np.frombuffer(raw_image, np.uint8).reshape([rows, cols, 3])
            cv2.imshow('Video', image) # Show video image for testing
            cv2.waitKey(1)
        counter += 1



# https://github.com/paretech/klvdata/tree/master/klvdata
def bytes_to_int(value, signed=False):
    """Return integer given bytes."""
    return int.from_bytes(bytes(value), byteorder='big', signed=signed)


# Data reader thread (read KLV data).
def data_reader(pipe):
    key_length = 16  # Assume key length is 16 bytes.

    f = open('data.bin', 'wb')  # For testing - store the KLV data to data.bin (binary file)

    while True:
        # https://en.wikipedia.org/wiki/KLV
        # The first few bytes are the Key, much like a key in a standard hash table data structure.
        # Keys can be 1, 2, 4, or 16 bytes in length.
        # Presumably in a separate specification document you would agree on a key length for a given application.
        key = pipe.read(key_length)  # Read the key
        
        if len(key) < key_length:
            break  # Break the loop when length is too small
        f.write(key)  # Write data to binary file for testing

        # https://github.com/paretech/klvdata/tree/master/klvdata
        # Length field
        len_byte = pipe.read(1)

        if len(len_byte) < 1:
            break  # Break the loop when length is too small
        f.write(len_byte)  # Write data to binary file for testing

        byte_length = bytes_to_int(len_byte)

        # https://github.com/paretech/klvdata/tree/master/klvdata                                                
        if byte_length < 128:
            # BER Short Form
            length = byte_length
            ber_len_bytes = b''
        else:
            # BER Long Form
            ber_len = byte_length - 128
            ber_len_bytes = pipe.read(ber_len)

            if len(ber_len_bytes) < ber_len:
                break  # Break the loop when length is too small
            f.write(ber_len_bytes)  # Write ber_len_bytes to binary file for testing

            length = bytes_to_int(ber_len_bytes)

        # Read the value (length bytes)
        value = pipe.read(length)
        if len(value) < length:
            break  # Break the loop when length is too small
        f.write(value)  # Write data to binary file for testing

        klv_data = key + len_byte + ber_len_bytes + value  # Concatenate key length and data
        klv_data_as_bytes_io = BytesIO(klv_data)  # Wrap klv_data with BytesIO (before parsing)

        # Parse the KLV data
        for packet in klvdata.StreamParser(klv_data_as_bytes_io): 
            metadata = packet.MetadataList()
            print(metadata)
            print() # New line

# Execute FFmpeg as sub-process
# Map the video to stderr and map the data to stdout
process = sp.Popen(shlex.split('ffmpeg -hide_banner -loglevel quiet '                        # Set loglevel to quiet for disabling the prints ot stderr
                               '-i "Day Flight.mpg" '                                        # Input video "Day Flight.mpg"
                               '-map 0:v -c:v rawvideo -pix_fmt bgr24 -f:v rawvideo pipe:2 ' # rawvideo format is mapped to stderr pipe (raw video codec with bgr24 pixel format)
                               '-map 0:d -c copy -copy_unknown -f:d data pipe:1 '            # Copy the data without ddecoding.
                               '-report'),                                                   # Create a log file (because we can't the statuses that are usually printed to stderr).
                                stdout=sp.PIPE, stderr=sp.PIPE)


# Start video reader thread (pass stderr pipe as argument).
video_thread = threading.Thread(target=video_reader, args=(process.stderr,))
video_thread.start()

# Start data reader thread (pass stdout pipe as argument).
data_thread = threading.Thread(target=data_reader, args=(process.stdout,))
data_thread.start()


# Wait for threads (and process) to finish.
video_thread.join()
data_thread.join()
process.wait()

The above code saves the data to data.bin (for testing).
data.bin may be used for consistency check.
Execute FFmpeg CLI for extracting the data stream:

ffmpeg -y -i "Day Flight.mpg" -map 0:d -c copy -copy_unknown -f data raw.bin

Verify that raw.bin and data.bin files are equal.

Rotem
  • 30,366
  • 4
  • 32
  • 65
  • Thanks!! Your solution/code worked like a charm. BTW, I noticed the difference between the klv packets and frames but ultimate goal is to process a udp stream but you have given more than enough to work with. Thanks again! – seabass1217 Mar 16 '22 at 11:59