
Recherche avancée
Autres articles (11)
-
Contribute to a better visual interface
13 avril 2011MediaSPIP is based on a system of themes and templates. Templates define the placement of information on the page, and can be adapted to a wide range of uses. Themes define the overall graphic appearance of the site.
Anyone can submit a new graphic theme or template and make it available to the MediaSPIP community. -
Ajouter des informations spécifiques aux utilisateurs et autres modifications de comportement liées aux auteurs
12 avril 2011, parLa manière la plus simple d’ajouter des informations aux auteurs est d’installer le plugin Inscription3. Il permet également de modifier certains comportements liés aux utilisateurs (référez-vous à sa documentation pour plus d’informations).
Il est également possible d’ajouter des champs aux auteurs en installant les plugins champs extras 2 et Interface pour champs extras. -
Qu’est ce qu’un éditorial
21 juin 2013, parEcrivez votre de point de vue dans un article. Celui-ci sera rangé dans une rubrique prévue à cet effet.
Un éditorial est un article de type texte uniquement. Il a pour objectif de ranger les points de vue dans une rubrique dédiée. Un seul éditorial est placé à la une en page d’accueil. Pour consulter les précédents, consultez la rubrique dédiée.
Vous pouvez personnaliser le formulaire de création d’un éditorial.
Formulaire de création d’un éditorial Dans le cas d’un document de type éditorial, les (...)
Sur d’autres sites (4352)
-
How to play and seek fragmented MP4 audio using MSE SourceBuffer ?
29 juin 2024, par Stefan FalkNote :




If you end up here, you might want to take a look at shaka-player and the accompanying shaka-streamer. Use it. Don't implement this yourself unless you really have to.




I am trying for quite some time now to be able to play an audio track on Chrome, Firefox, Safari, etc. but I keep hitting brick walls. My problem is currently that I am just not able to seek within a fragmented MP4 (or MP3).


At the moment I am converting audio files such as MP3 to fragmented MP4 (fMP4) and send them chunk-wise to the client. What I do is defining a
CHUNK_DURACTION_SEC
(chunk duration in seconds) and compute a chunk size like this :

chunksTotal = Math.ceil(this.track.duration / CHUNK_DURATION_SEC);
chunkSize = Math.ceil(this.track.fileSize / this.chunksTotal);



With this I partition the audio file and can fetch it entirely jumping
chunkSize
-many bytes for each chunk :

-----------------------------------------
| chunk 1 | chunk 2 | ... | chunk n |
-----------------------------------------



How audio files are converted to fMP4


ffmpeg -i input.mp3 -acodec aac -b:a 256k -f mp4 \
 -movflags faststart+frag_every_frame+empty_moov+default_base_moof \
 output.mp4



This seems to work with Chrome and Firefox (so far).


How chunks are appended


After following this example, and realizing that it's simply not working as it is explained here, I threw it away and started over from scratch. Unfortunately without success. It's still not working.


The following code is supposed to play a track from the very beginning to the very end. However, I also need to be able to seek. So far, this is simply not working. Seeking will just stop the audio after the
seeking
event got triggered.

The code


/* Desired chunk duration in seconds. */
const CHUNK_DURATION_SEC = 20;

const AUDIO_EVENTS = [
 'ended',
 'error',
 'play',
 'playing',
 'seeking',
 'seeked',
 'pause',
 'timeupdate',
 'canplay',
 'loadedmetadata',
 'loadstart',
 'updateend',
];


class ChunksLoader {

 /** The total number of chunks for the track. */
 public readonly chunksTotal: number;

 /** The length of one chunk in bytes */
 public readonly chunkSize: number;

 /** Keeps track of requested chunks. */
 private readonly requested: boolean[];

 /** URL of endpoint for fetching audio chunks. */
 private readonly url: string;

 constructor(
 private track: Track,
 private sourceBuffer: SourceBuffer,
 private logger: NGXLogger,
 ) {

 this.chunksTotal = Math.ceil(this.track.duration / CHUNK_DURATION_SEC);
 this.chunkSize = Math.ceil(this.track.fileSize / this.chunksTotal);

 this.requested = [];
 for (let i = 0; i < this.chunksTotal; i++) {
 this.requested[i] = false;
 }

 this.url = `${environment.apiBaseUrl}/api/tracks/${this.track.id}/play`;
 }

 /**
 * Fetch the first chunk.
 */
 public begin() {
 this.maybeFetchChunk(0);
 }

 /**
 * Handler for the "timeupdate" event. Checks if the next chunk should be fetched.
 *
 * @param currentTime
 * The current time of the track which is currently played.
 */
 public handleOnTimeUpdate(currentTime: number) {

 const nextChunkIndex = Math.floor(currentTime / CHUNK_DURATION_SEC) + 1;
 const hasAllChunks = this.requested.every(val => !!val);

 if (nextChunkIndex === (this.chunksTotal - 1) && hasAllChunks) {
 this.logger.debug('Last chunk. Calling mediaSource.endOfStream();');
 return;
 }

 if (this.requested[nextChunkIndex] === true) {
 return;
 }

 if (currentTime < CHUNK_DURATION_SEC * (nextChunkIndex - 1 + 0.25)) {
 return;
 }

 this.maybeFetchChunk(nextChunkIndex);
 }

 /**
 * Fetches the chunk if it hasn't been requested yet. After the request finished, the returned
 * chunk gets appended to the SourceBuffer-instance.
 *
 * @param chunkIndex
 * The chunk to fetch.
 */
 private maybeFetchChunk(chunkIndex: number) {

 const start = chunkIndex * this.chunkSize;
 const end = start + this.chunkSize - 1;

 if (this.requested[chunkIndex] == true) {
 return;
 }

 this.requested[chunkIndex] = true;

 if ((end - start) == 0) {
 this.logger.warn('Nothing to fetch.');
 return;
 }

 const totalKb = ((end - start) / 1000).toFixed(2);
 this.logger.debug(`Starting to fetch bytes ${start} to ${end} (total ${totalKb} kB). Chunk ${chunkIndex + 1} of ${this.chunksTotal}`);

 const xhr = new XMLHttpRequest();
 xhr.open('get', this.url);
 xhr.setRequestHeader('Authorization', `Bearer ${AuthenticationService.getJwtToken()}`);
 xhr.setRequestHeader('Range', 'bytes=' + start + '-' + end);
 xhr.responseType = 'arraybuffer';
 xhr.onload = () => {
 this.logger.debug(`Range ${start} to ${end} fetched`);
 this.logger.debug(`Requested size: ${end - start + 1}`);
 this.logger.debug(`Fetched size: ${xhr.response.byteLength}`);
 this.logger.debug('Appending chunk to SourceBuffer.');
 this.sourceBuffer.appendBuffer(xhr.response);
 };
 xhr.send();
 };

}

export enum StreamStatus {
 NOT_INITIALIZED,
 INITIALIZING,
 PLAYING,
 SEEKING,
 PAUSED,
 STOPPED,
 ERROR
}

export class PlayerState {
 status: StreamStatus = StreamStatus.NOT_INITIALIZED;
}


/**
 *
 */
@Injectable({
 providedIn: 'root'
})
export class MediaSourcePlayerService {

 public track: Track;

 private mediaSource: MediaSource;

 private sourceBuffer: SourceBuffer;

 private audioObj: HTMLAudioElement;

 private chunksLoader: ChunksLoader;

 private state: PlayerState = new PlayerState();

 private state$ = new BehaviorSubject<playerstate>(this.state);

 public stateChange = this.state$.asObservable();

 private currentTime$ = new BehaviorSubject<number>(null);

 public currentTimeChange = this.currentTime$.asObservable();

 constructor(
 private httpClient: HttpClient,
 private logger: NGXLogger
 ) {
 }

 get canPlay() {
 const state = this.state$.getValue();
 const status = state.status;
 return status == StreamStatus.PAUSED;
 }

 get canPause() {
 const state = this.state$.getValue();
 const status = state.status;
 return status == StreamStatus.PLAYING || status == StreamStatus.SEEKING;
 }

 public playTrack(track: Track) {
 this.logger.debug('playTrack');
 this.track = track;
 this.startPlayingFrom(0);
 }

 public play() {
 this.logger.debug('play()');
 this.audioObj.play().then();
 }

 public pause() {
 this.logger.debug('pause()');
 this.audioObj.pause();
 }

 public stop() {
 this.logger.debug('stop()');
 this.audioObj.pause();
 }

 public seek(seconds: number) {
 this.logger.debug('seek()');
 this.audioObj.currentTime = seconds;
 }

 private startPlayingFrom(seconds: number) {
 this.logger.info(`Start playing from ${seconds.toFixed(2)} seconds`);
 this.mediaSource = new MediaSource();
 this.mediaSource.addEventListener('sourceopen', this.onSourceOpen);

 this.audioObj = document.createElement('audio');
 this.addEvents(this.audioObj, AUDIO_EVENTS, this.handleEvent);
 this.audioObj.src = URL.createObjectURL(this.mediaSource);

 this.audioObj.play().then();
 }

 private onSourceOpen = () => {

 this.logger.debug('onSourceOpen');

 this.mediaSource.removeEventListener('sourceopen', this.onSourceOpen);
 this.mediaSource.duration = this.track.duration;

 this.sourceBuffer = this.mediaSource.addSourceBuffer('audio/mp4; codecs="mp4a.40.2"');
 // this.sourceBuffer = this.mediaSource.addSourceBuffer('audio/mpeg');

 this.chunksLoader = new ChunksLoader(
 this.track,
 this.sourceBuffer,
 this.logger
 );

 this.chunksLoader.begin();
 };

 private handleEvent = (e) => {

 const currentTime = this.audioObj.currentTime.toFixed(2);
 const totalDuration = this.track.duration.toFixed(2);
 this.logger.warn(`MediaSource event: ${e.type} (${currentTime} of ${totalDuration} sec)`);

 this.currentTime$.next(this.audioObj.currentTime);

 const currentStatus = this.state$.getValue();

 switch (e.type) {
 case 'playing':
 currentStatus.status = StreamStatus.PLAYING;
 this.state$.next(currentStatus);
 break;
 case 'pause':
 currentStatus.status = StreamStatus.PAUSED;
 this.state$.next(currentStatus);
 break;
 case 'timeupdate':
 this.chunksLoader.handleOnTimeUpdate(this.audioObj.currentTime);
 break;
 case 'seeking':
 currentStatus.status = StreamStatus.SEEKING;
 this.state$.next(currentStatus);
 if (this.mediaSource.readyState == 'open') {
 this.sourceBuffer.abort();
 }
 this.chunksLoader.handleOnTimeUpdate(this.audioObj.currentTime);
 break;
 }
 };

 private addEvents(obj, events, handler) {
 events.forEach(event => obj.addEventListener(event, handler));
 }

}
</number></playerstate>


Running it will give me the following output :






Apologies for the screenshot but it's not possible to just copy the output without all the stack traces in Chrome.




What I also tried was following this example and call
sourceBuffer.abort()
but that didn't work. It looks more like a hack that used to work years ago but it's still referenced in the docs (see "Example" -> "You can see something similar in action in Nick Desaulnier's bufferWhenNeeded demo ..").

case 'seeking':
 currentStatus.status = StreamStatus.SEEKING;
 this.state$.next(currentStatus); 
 if (this.mediaSource.readyState === 'open') {
 this.sourceBuffer.abort();
 } 
 break;



Trying with MP3


I have tested the above code under Chrome by converting tracks to MP3 :


ffmpeg -i input.mp3 -acodec aac -b:a 256k -f mp3 output.mp3



and creating a
SourceBuffer
usingaudio/mpeg
as type :

this.mediaSource.addSourceBuffer('audio/mpeg')



I have the same problem when seeking.


The issue wihout seeking


The above code has another issue :


After two minutes of playing, the audio playback starts to stutter and comes to a halt prematurely. So, the audio plays up to a point and then it stops without any obvious reason.


For whatever reason there is another
canplay
andplaying
event. A few seconds after, the audio simply stops..



-
ffmpeg takes too long to start
17 octobre 2020, par SuspendedI have this command in python script, in a loop :


ffmpeg -i somefile.mp4 -ss 00:03:12 -t 00:00:35 piece.mp4 -loglevel error -stats



It cuts out pieces of input file (-i). Input filename, as well as start time (-ss) and length of the piece I cut out (-t) varies, so it reads number of mp4 files and cuts out number of pieces from each one. During execution of the script it might be called around 100 times. My problem is that each time before it starts, there is a delay of 6-15 seconds and it adds up to significant time. How can I get it to start immediately ?


Initially I thought it was process priority problem, but I noticed that even during the "pause", all processors work at 100%, so apparently some work is being done.


The script (process_videos.py) :


import subprocess
import sys
import math
import time

class TF:
 """TimeFormatter class (TF).
This class' reason for being is to convert time in short
form, e.g. 1:33, 0:32, or 23 into long form accepted by
mp4cut function in bash, e.g. 00:01:22, 00:00:32, etc"""

def toLong(self, shrt):
 """Converts time to its long form"""
 sx = '00:00:00'
 ladd = 8 - len(shrt)
 n = sx[:ladd] + shrt
 return n

def toShort(self, lng):
 """Converts time to short form"""
 if lng[0] == '0' or lng[0] == ':':
 return self.toShort(lng[1:])
 else:
 return lng

def toSeconds(self, any_time):
 """Converts time to seconds"""
 if len(any_time) < 3:
 return int(any_time)
 tt = any_time.split(':')
 if len(any_time) < 6: 
 return int(tt[0])*60 + int(tt[1])
 return int(tt[0])*3600 + int(tt[1])*60 + int(tt[2])

def toTime(self, secsInt):
 """"""
 tStr = ''
 hrs, mins, secs = 0, 0, 0
 if secsInt >= 3600:
 hrs = math.floor(secsInt / 3600)
 secsInt = secsInt % 3600
 if secsInt >= 60:
 mins = math.floor(secsInt / 60)
 secsInt = secsInt % 60
 secs = secsInt
 return str(hrs).zfill(2) + ':' + str(mins).zfill(2) + ':' + str(secs).zfill(2)

def minus(self, t_start, t_end):
 """"""
 t_e = self.toSeconds(t_end)
 t_s = self.toSeconds(t_start)
 t_r = t_e - t_s
 hrs, mins, secs = 0, 0, 0
 if t_r >= 3600:
 hrs = math.floor(t_r / 3600)
 t_r = t_r - (hrs * 3600)
 if t_r >= 60:
 mins = math.floor(t_r / 60)
 t_r = t_r - (mins * 60)
 secs = t_r
 hrsf = str(hrs).zfill(2)
 minsf = str(mins).zfill(2)
 secsf = str(secs).zfill(2)
 t_fnl = hrsf + ':' + minsf + ':' + secsf
 return t_fnl

def go_main():
 tf = TF()
 vid_n = 0
 arglen = len(sys.argv)
 if arglen == 2:
 with open(sys.argv[1], 'r') as f_in:
 lines = f_in.readlines()
 start = None
 end = None
 cnt = 0
 for line in lines:
 if line[:5] == 'BEGIN':
 start = cnt
 if line[:3] == 'END':
 end = cnt
 cnt += 1
 if start == None or end == None:
 print('Invalid file format. start = {}, end = {}'.format(start,end))
 return
 else:
 lines_r = lines[start+1:end]
 del lines
 print('videos to process: {}'.format(len(lines_r)))
 f_out_prefix = ""
 for vid in lines_r:
 vid_n += 1
 print('\nProcessing video {}/{}'.format(vid_n, len(lines_r)))
 f_out_prefix = 'v' + str(vid_n) + '-'
 dat = vid.split('!')[1:3]
 title = dat[0]
 dat_t = dat[1].split(',')
 v_pieces = len(dat_t)
 piece_n = 0
 video_pieces = []
 cmd1 = "echo -n \"\" > tmpfile"
 subprocess.run(cmd1, shell=True) 
 print(' new tmpfile created')
 for v_times in dat_t:
 piece_n += 1
 f_out = f_out_prefix + str(piece_n) + '.mp4'
 video_pieces.append(f_out)
 print(' piece filename {} added to video_pieces list'.format(f_out))
 v_times_spl = v_times.split('-')
 v_times_start = v_times_spl[0]
 v_times_end = v_times_spl[1]
 t_st = tf.toLong(v_times_start)
 t_dur = tf.toTime(tf.toSeconds(v_times_end) - tf.toSeconds(v_times_start))
 cmd3 = ["ffmpeg", "-i", title, "-ss", t_st, "-t", t_dur, f_out, "-loglevel", "error", "-stats"]
 print(' cutting out piece {}/{} - {}'.format(piece_n, len(dat_t), t_dur))
 subprocess.run(cmd3)
 for video_piece_name in video_pieces:
 cmd4 = "echo \"file " + video_piece_name + "\" >> tmpfile"
 subprocess.run(cmd4, shell=True)
 print(' filename {} added to tmpfile'.format(video_piece_name))
 vname = f_out_prefix[:-1] + ".mp4"
 print(' name of joined file: {}'.format(vname))
 cmd5 = "ffmpeg -f concat -safe 0 -i tmpfile -c copy joined.mp4 -loglevel error -stats"
 to_be_joined = " ".join(video_pieces)
 print(' joining...')
 join_cmd = subprocess.Popen(cmd5, shell=True)
 join_cmd.wait()
 print(' joined!')
 cmd6 = "mv joined.mp4 " + vname
 rename_cmd = subprocess.Popen(cmd6, shell=True)
 rename_cmd.wait()
 print(' File joined.mp4 renamed to {}'.format(vname))
 cmd7 = "rm " + to_be_joined
 rm_cmd = subprocess.Popen(cmd7, shell=True)
 rm_cmd.wait()
 print('rm command completed - pieces removed')
 cmd8 = "rm tmpfile"
 subprocess.run(cmd8, shell=True)
 print('tmpfile removed')
 print('All done')
 else:
 print('Incorrect number of arguments')

############################
if __name__ == '__main__':
 go_main()



process_videos.py is called from bash terminal like this :


$ python process_videos.py video_data 



video_data file has the following format :


BEGIN
!first_video.mp4!3-23,55-1:34,2:01-3:15,3:34-3:44!
!second_video.mp4!2-7,12-44,1:03-1:33!
END



My system details :


System: Host: snowflake Kernel: 5.4.0-52-generic x86_64 bits: 64 Desktop: Gnome 3.28.4
 Distro: Ubuntu 18.04.5 LTS
Machine: Device: desktop System: Gigabyte product: N/A serial: N/A
Mobo: Gigabyte model: Z77-D3H v: x.x serial: N/A BIOS: American Megatrends v: F14 date: 05/31/2012
CPU: Quad core Intel Core i5-3570 (-MCP-) cache: 6144 KB 
 clock speeds: max: 3800 MHz 1: 1601 MHz 2: 1601 MHz 3: 1601 MHz 4: 1602 MHz
Drives: HDD Total Size: 1060.2GB (55.2% used)
 ID-1: /dev/sda model: ST31000524AS size: 1000.2GB
 ID-2: /dev/sdb model: Corsair_Force_GT size: 60.0GB
Partition: ID-1: / size: 366G used: 282G (82%) fs: ext4 dev: /dev/sda1
 ID-2: swap-1 size: 0.70GB used: 0.00GB (0%) fs: swap dev: /dev/sda5
Info: Processes: 313 Uptime: 16:37 Memory: 3421.4/15906.9MB Client: Shell (bash) inxi: 2.3.56




UPDATE :


Following Charles' advice, I used performance sampling :


# perf record -a -g sleep 180



...and here's the report :


Samples: 74K of event 'cycles', Event count (approx.): 1043554519767
 Children Self Command Shared Object
- 50.56% 45.86% ffmpeg libavcodec.so.57.107.100 
 - 3.10% 0x4489480000002825 
 0.64% 0x7ffaf24b92f0 
 - 2.12% 0x5f7369007265646f 
 av_default_item_name 
 1.39% 0 
- 44.48% 40.59% ffmpeg libx264.so.152 
 5.78% x264_add8x8_idct_avx2.skip_prologue 
 3.13% x264_add8x8_idct_avx2.skip_prologue 
 2.91% x264_add8x8_idct_avx2.skip_prologue 
 2.31% x264_add8x8_idct_avx.skip_prologue 
 2.03% 0 
 1.78% 0x1 
 1.26% x264_add8x8_idct_avx2.skip_prologue 
 1.09% x264_add8x8_idct_avx.skip_prologue 
 1.06% x264_me_search_ref 
 0.97% x264_add8x8_idct_avx.skip_prologue 
 0.60% x264_me_search_ref 
- 38.01% 0.00% ffmpeg [unknown] 
 4.10% 0 
 - 3.49% 0x4489480000002825 
 0.70% 0x7ffaf24b92f0 
 0.56% 0x7f273ae822f0 
 0.50% 0x7f0c4768b2f0 
 - 2.29% 0x5f7369007265646f 
 av_default_item_name 
 1.99% 0x1 
 10.13% 10.12% ffmpeg [kernel.kallsyms] 
- 3.14% 0.73% ffmpeg libavutil.so.55.78.100 
 2.34% av_default_item_name 
- 1.73% 0.21% ffmpeg libpthread-2.27.so 
 - 0.70% pthread_cond_wait@@GLIBC_2.3.2 
 - 0.62% entry_SYSCALL_64_after_hwframe 
 - 0.62% do_syscall_64 
 - 0.57% __x64_sys_futex 
 0.52% do_futex 
 0.93% 0.89% ffmpeg libc-2.27.so 
- 0.64% 0.64% swapper [kernel.kallsyms] 
 0.63% secondary_startup_64 
 0.21% 0.18% ffmpeg libavfilter.so.6.107.100 
 0.20% 0.11% ffmpeg libavformat.so.57.83.100 
 0.12% 0.11% ffmpeg ffmpeg 
 0.11% 0.00% gnome-terminal- [unknown] 
 0.09% 0.07% ffmpeg libm-2.27.so 
 0.08% 0.07% ffmpeg ld-2.27.so 
 0.04% 0.04% gnome-terminal- libglib-2.0.so.0.5600.4





-
ffmpeg takes a while to start
17 octobre 2020, par SuspendedI have this command in python script, in a loop :


ffmpeg -i somefile.mp4 -ss 00:03:12 -t 00:00:35 piece.mp4 -loglevel error -stats



It cuts out pieces of input file (-i). Input filename, as well as start time (-ss) and length of the piece I cut out (-t) varies, so it reads number of mp4 files and cuts out number of pieces from each one. During execution of the script it might be called around 100 times. My problem is that each time before it starts, there is a delay of few seconds and it adds up to significant time. How can I get it to start immediately ?


The script (process_videos.py) :


import subprocess
import sys
import math
import time

class TF:
 """TimeFormatter class (TF).
This class' reason for being is to convert time in short
form, e.g. 1:33, 0:32, or 23 into long form accepted by
mp4cut function in bash, e.g. 00:01:22, 00:00:32, etc"""

def toLong(self, shrt):
 """Converts time to its long form"""
 sx = '00:00:00'
 ladd = 8 - len(shrt)
 n = sx[:ladd] + shrt
 return n

def toShort(self, lng):
 """Converts time to short form"""
 if lng[0] == '0' or lng[0] == ':':
 return self.toShort(lng[1:])
 else:
 return lng

def toSeconds(self, any_time):
 """Converts time to seconds"""
 if len(any_time) < 3:
 return int(any_time)
 tt = any_time.split(':')
 if len(any_time) < 6: 
 return int(tt[0])*60 + int(tt[1])
 return int(tt[0])*3600 + int(tt[1])*60 + int(tt[2])

def toTime(self, secsInt):
 """"""
 tStr = ''
 hrs, mins, secs = 0, 0, 0
 if secsInt >= 3600:
 hrs = math.floor(secsInt / 3600)
 secsInt = secsInt % 3600
 if secsInt >= 60:
 mins = math.floor(secsInt / 60)
 secsInt = secsInt % 60
 secs = secsInt
 return str(hrs).zfill(2) + ':' + str(mins).zfill(2) + ':' + str(secs).zfill(2)

def minus(self, t_start, t_end):
 """"""
 t_e = self.toSeconds(t_end)
 t_s = self.toSeconds(t_start)
 t_r = t_e - t_s
 hrs, mins, secs = 0, 0, 0
 if t_r >= 3600:
 hrs = math.floor(t_r / 3600)
 t_r = t_r - (hrs * 3600)
 if t_r >= 60:
 mins = math.floor(t_r / 60)
 t_r = t_r - (mins * 60)
 secs = t_r
 hrsf = str(hrs).zfill(2)
 minsf = str(mins).zfill(2)
 secsf = str(secs).zfill(2)
 t_fnl = hrsf + ':' + minsf + ':' + secsf
 return t_fnl

def go_main():
 tf = TF()
 vid_n = 0
 arglen = len(sys.argv)
 if arglen == 2:
 with open(sys.argv[1], 'r') as f_in:
 lines = f_in.readlines()
 start = None
 end = None
 cnt = 0
 for line in lines:
 if line[:5] == 'BEGIN':
 start = cnt
 if line[:3] == 'END':
 end = cnt
 cnt += 1
 if start == None or end == None:
 print('Invalid file format. start = {}, end = {}'.format(start,end))
 return
 else:
 lines_r = lines[start+1:end]
 del lines
 print('videos to process: {}'.format(len(lines_r)))
 f_out_prefix = ""
 for vid in lines_r:
 vid_n += 1
 print('\nProcessing video {}/{}'.format(vid_n, len(lines_r)))
 f_out_prefix = 'v' + str(vid_n) + '-'
 dat = vid.split('!')[1:3]
 title = dat[0]
 dat_t = dat[1].split(',')
 v_pieces = len(dat_t)
 piece_n = 0
 video_pieces = []
 cmd1 = "echo -n \"\" > tmpfile"
 subprocess.run(cmd1, shell=True) 
 print(' new tmpfile created')
 for v_times in dat_t:
 piece_n += 1
 f_out = f_out_prefix + str(piece_n) + '.mp4'
 video_pieces.append(f_out)
 print(' piece filename {} added to video_pieces list'.format(f_out))
 v_times_spl = v_times.split('-')
 v_times_start = v_times_spl[0]
 v_times_end = v_times_spl[1]
 t_st = tf.toLong(v_times_start)
 t_dur = tf.toTime(tf.toSeconds(v_times_end) - tf.toSeconds(v_times_start))
 cmd3 = ["ffmpeg", "-i", title, "-ss", t_st, "-t", t_dur, f_out, "-loglevel", "error", "-stats"]
 print(' cutting out piece {}/{} - {}'.format(piece_n, len(dat_t), t_dur))
 subprocess.run(cmd3)
 for video_piece_name in video_pieces:
 cmd4 = "echo \"file " + video_piece_name + "\" >> tmpfile"
 subprocess.run(cmd4, shell=True)
 print(' filename {} added to tmpfile'.format(video_piece_name))
 vname = f_out_prefix[:-1] + ".mp4"
 print(' name of joined file: {}'.format(vname))
 cmd5 = "ffmpeg -f concat -safe 0 -i tmpfile -c copy joined.mp4 -loglevel error -stats"
 to_be_joined = " ".join(video_pieces)
 print(' joining...')
 join_cmd = subprocess.Popen(cmd5, shell=True)
 join_cmd.wait()
 print(' joined!')
 cmd6 = "mv joined.mp4 " + vname
 rename_cmd = subprocess.Popen(cmd6, shell=True)
 rename_cmd.wait()
 print(' File joined.mp4 renamed to {}'.format(vname))
 cmd7 = "rm " + to_be_joined
 rm_cmd = subprocess.Popen(cmd7, shell=True)
 rm_cmd.wait()
 print('rm command completed - pieces removed')
 cmd8 = "rm tmpfile"
 subprocess.run(cmd8, shell=True)
 print('tmpfile removed')
 print('All done')
 else:
 print('Incorrect number of arguments')

############################
if __name__ == '__main__':
 go_main()



process_videos.py is called from bash terminal like this :


$ python process_videos.py video_data 



video_data file has the following format :


BEGIN
!first_video.mp4!3-23,55-1:34,2:01-3:15,3:34-3:44!
!second_video.mp4!2-7,12-44,1:03-1:33!
END



My system details :


System: Host: snowflake Kernel: 5.4.0-52-generic x86_64 bits: 64 Desktop: Gnome 3.28.4
 Distro: Ubuntu 18.04.5 LTS
Machine: Device: desktop System: Gigabyte product: N/A serial: N/A
Mobo: Gigabyte model: Z77-D3H v: x.x serial: N/A BIOS: American Megatrends v: F14 date: 05/31/2012
CPU: Quad core Intel Core i5-3570 (-MCP-) cache: 6144 KB 
 clock speeds: max: 3800 MHz 1: 1601 MHz 2: 1601 MHz 3: 1601 MHz 4: 1602 MHz
Drives: HDD Total Size: 1060.2GB (55.2% used)
 ID-1: /dev/sda model: ST31000524AS size: 1000.2GB
 ID-2: /dev/sdb model: Corsair_Force_GT size: 60.0GB
Partition: ID-1: / size: 366G used: 282G (82%) fs: ext4 dev: /dev/sda1
 ID-2: swap-1 size: 0.70GB used: 0.00GB (0%) fs: swap dev: /dev/sda5
Info: Processes: 313 Uptime: 16:37 Memory: 3421.4/15906.9MB Client: Shell (bash) inxi: 2.3.56