0

I am trying to save video and audio simultaneously and periodically into mp4 files of fixed time length. I know in order to achieve this goal, I have to run two individual threads, one recording a video and the other recording the audio in parallel, thanks to the post How to capture a video (AND audio) in python, from a camera (or webcam). I have modified the code as below, now it can save the video and audio simultaneously into separate files of fixed duration. My question is, how can I merge the video and audio files on the fly soon as they are generated by the two individual threads? I can merge the video and audio files after the two threadings are done, but this is not what I want.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# VideoRecorder.py

from __future__ import print_function, division
import numpy as np
import cv2
import pyaudio
import wave
import threading
import time
from datetime import datetime
import subprocess
import os
import ffmpeg

LOOP_DURATION = 5


class VideoRecorder():
    "Video class based on openCV"
    def __init__(self, name="temp_video.avi", fourcc="MJPG", sizex=640, sizey=480, camindex=0, fps=10):
        self.open = True
        self.device_index = camindex
        self.fps = fps                  # fps should be the minimum constant rate at which the camera can
        self.fourcc = fourcc            # capture images (with no decrease in speed over time; testing is required)
        self.frameSize = (sizex, sizey) # video formats and sizes also depend and vary according to the camera used
        self.video_filename = name
        self.video_cap = cv2.VideoCapture(self.device_index)
        self.video_cap.set(cv2.CAP_PROP_FPS, self.fps)
        self.video_writer = cv2.VideoWriter_fourcc(*self.fourcc)
        self.video_out = cv2.VideoWriter(self.video_filename, self.video_writer, self.fps, self.frameSize)
        self.frame_counts = 1
        self.start_time = time.time()

    def record(self):
        "Video starts being recorded"
        # counter = 1
        timer_start = time.time()
        timer_current = 0
        while self.open:
            start = time.time()
            now = datetime.now()
            if not os.path.isdir("output"):
                os.mkdir("output")
            self.video_filename = os.path.join("./output", now.strftime("%y-%m-%d-%H_%M_%S") + '.avi')
            self.video_out = cv2.VideoWriter(self.video_filename, self.video_writer, self.fps, self.frameSize)
            while time.time() - start < LOOP_DURATION:
                ret, video_frame = self.video_cap.read()
                self.video_out.write(video_frame)


            # if ret:
            #     self.video_out.write(video_frame)
            #     # print(str(counter) + " " + str(self.frame_counts) + " frames written " + str(timer_current))
            #     self.frame_counts += 1
            #     # print(self.frame_counts)
            #     # counter += 1
            #     # timer_current = time.time() - timer_start
            #     # time.sleep(1/self.fps)
            #     # gray = cv2.cvtColor(video_frame, cv2.COLOR_BGR2GRAY)
            #     # cv2.imshow('video_frame', video_frame)
            #     # cv2.waitKey(1)
            # else:
            #     break

    def stop(self):
        "Finishes the video recording therefore the thread too"
        if self.open:
            self.open=False
            self.video_out.release()
            self.video_cap.release()
            cv2.destroyAllWindows()

    def start(self):
        "Launches the video recording function using a thread"
        video_thread = threading.Thread(target=self.record)
        video_thread.start()

class AudioRecorder():
    "Audio class based on pyAudio and Wave"
    def __init__(self, filename="temp_audio.wav", rate=44100, fpb=1024, channels=2):
        self.open = True
        self.rate = rate
        self.frames_per_buffer = fpb
        self.channels = channels
        self.format = pyaudio.paInt16
        self.audio_filename = filename
        self.audio = pyaudio.PyAudio()
        self.stream = self.audio.open(format=self.format,
                                      channels=self.channels,
                                      rate=self.rate,
                                      input=True,
                                      frames_per_buffer = self.frames_per_buffer)
        self.audio_frames = []

    def record(self):
        "Audio starts being recorded"
        self.stream.start_stream()
        while self.open:
            start = time.time()
            now = datetime.now()
            if not os.path.isdir("output"):
                os.mkdir("output")
            self.audio_filename = os.path.join("./output", now.strftime("%y-%m-%d-%H_%M_%S") + '.wav')
            while time.time() - start < LOOP_DURATION:
                data = self.stream.read(self.frames_per_buffer)
                self.audio_frames.append(data)
            waveFile = wave.open(self.audio_filename, 'wb')
            waveFile.setnchannels(self.channels)
            waveFile.setsampwidth(self.audio.get_sample_size(self.format))
            waveFile.setframerate(self.rate)
            waveFile.writeframes(b''.join(self.audio_frames))
            waveFile.close()
            self.audio_frames = []
            if not self.open:
                break

    def stop(self):
        "Finishes the audio recording therefore the thread too"
        if self.open:
            self.open = False
            self.stream.stop_stream()
            self.stream.close()
            self.audio.terminate()
            waveFile = wave.open(self.audio_filename, 'wb')
            waveFile.setnchannels(self.channels)
            waveFile.setsampwidth(self.audio.get_sample_size(self.format))
            waveFile.setframerate(self.rate)
            waveFile.writeframes(b''.join(self.audio_frames))
            waveFile.close()

    def start(self):
        "Launches the audio recording function using a thread"
        audio_thread = threading.Thread(target=self.record)
        audio_thread.start()

def start_AVrecording(filename="test"):
    global video_thread
    global audio_thread
    video_thread = VideoRecorder()
    audio_thread = AudioRecorder()
    audio_thread.start()
    video_thread.start()
    return filename


def stop_AVrecording(filename="test"):
    audio_thread.stop()
    frame_counts = video_thread.frame_counts
    elapsed_time = time.time() - video_thread.start_time
    recorded_fps = frame_counts / elapsed_time
    print("total frames " + str(frame_counts))
    print("elapsed time " + str(elapsed_time))
    print("recorded fps " + str(recorded_fps))
    video_thread.stop()

    # Makes sure the threads have finished
    while threading.active_count() > 1:
        print(f"threading.active_count(): {threading.active_count()}")
        time.sleep(1)

    video_stream = ffmpeg.input(video_thread.video_filename)
    audio_stream = ffmpeg.input(audio_thread.audio_filename)
    now = datetime.now()
    output_file = os.path.join("./output", now.strftime("%y-%m-%d-%H_%M_%S") + '.mp4')
    ffmpeg.output(audio_stream, video_stream, output_file).run(overwrite_output=True)

    # # Merging audio and video signal
    # if abs(recorded_fps - 6) >= 0.01:    # If the fps rate was higher/lower than expected, re-encode it to the expected
    #     print("Re-encoding")
    #     cmd = "ffmpeg -r " + str(recorded_fps) + " -i temp_video.avi -pix_fmt yuv420p -r 6 temp_video2.avi"
    #     subprocess.call(cmd, shell=True)
    #     print("Muxing")
    #     cmd = "ffmpeg -y -ac 2 -channel_layout stereo -i temp_audio.wav -i temp_video2.avi -pix_fmt yuv420p " + filename + ".avi"
    #     subprocess.call(cmd, shell=True)
    # else:
    #     print("Normal recording\nMuxing")
    #     cmd = "ffmpeg -y -ac 2 -channel_layout stereo -i temp_audio.wav -i temp_video.avi -pix_fmt yuv420p " + filename + ".avi"
    #     subprocess.call(cmd, shell=True)
    #     print("..")

def file_manager(filename="test"):
    "Required and wanted processing of final files"
    local_path = os.getcwd()
    if os.path.exists(str(local_path) + "/temp_audio.wav"):
        os.remove(str(local_path) + "/temp_audio.wav")
    if os.path.exists(str(local_path) + "/temp_video.avi"):
        os.remove(str(local_path) + "/temp_video.avi")
    if os.path.exists(str(local_path) + "/temp_video2.avi"):
        os.remove(str(local_path) + "/temp_video2.avi")
    # if os.path.exists(str(local_path) + "/" + filename + ".avi"):
    #     os.remove(str(local_path) + "/" + filename + ".avi")

if __name__ == '__main__':

    start_AVrecording()
    time.sleep(30)
    stop_AVrecording()

Albert G Lieu
  • 891
  • 8
  • 16
  • you could run another thread for merging data - and current threads could use `queue` to send information that files are ready for merging and new thread would check queue to see when it has to start merging new files. – furas Aug 10 '22 at 07:21
  • @furas thank you very much for sharing your wisdom, I still didn't fully get it. Let's say thread 1 is recording a video and thread 2 is recording audio every 10 seconds, I can set up a queue for thread1 and another queue for thread2, then use a third thread to merge output from queue1 and queue2, how to guarantee the output from q1 and q2 are synchronized? – Albert G Lieu Aug 11 '22 at 00:11
  • @furas could you please provide a pseudo-code as an answer? thank you very very much – Albert G Lieu Aug 11 '22 at 06:16
  • it can be the problem. You could send pair `(data, time_of_start_recording)` but it can still problem to join elements partially - it can be simpler to create full files and join them. – furas Aug 11 '22 at 10:43

0 Answers0