Export segments in copy or encode mode (topic by CoSpi)

Started by eumagga0x2a, August 22, 2022, 09:46:33 PM

Previous topic - Next topic

eumagga0x2a

CleanTalk had prevented user "CoSpi" from posting the question below, which I repost based on a PM.

======================================================================================

CoSpi wrote:

Hi everybody
I would like to export segments using copy mode if segment starts and ends on I-frame or encode mode if not using tinypy script.
I start with deleting everything from video file that I don't want preferring cuts on I-frames, but sometimes it has to be on P- or B-frames.

First problem is that segments are saved to project file with offset.
For example when I cut middle part of video, 2 segments are created, but first segment doesn't start on 0 pts but 125125 (3 frames offset) in one video file and 83917 (2 frames offset) in other video file. So the offset is not consistent. All segments in one video file are moved by same pts.
I need to find out the offset for each video file to subtract it from each segment so I can determinate if I can use copy mode or not.

Second problem is that I can't determinate if segment starts and ends on I-frame if the project contains any segments.
I need to use "Edit -> Reset Edit" to remove all segments and than I can test pts on I-frames.
adm.clearSegments() causes error so I used adm.loadVideo(ed.getRefVideoName(0)) to clear segments.

So what I have now:
 - I create segments
 - Loop over segments to get beginnings and durations of segments
 - Re-load video to clear all segments
 - Loop over saved segments pts to set adm.markerA and adm.markerB
 - Determinate if segment starts and ends on I-frame to set copy or encode mode
 - Save segment -> Third problem

Third problem: segment is not saved when mode is not copy but encode and markers are set with script not to I-frames. File contains only video file heather.
But when I use "Go to marker A" button and than save it, it works!
Ok so I just need to use adm.setCurrentPts(pts) to move to marker A, but adm.setCurrentPts(pts) moves to not to given pts, but to I-frame close to given pts. So the save doesn't work.

Any ideas how to solve this problems?

Thank you for your help.

I am using version 2.8.1 (220813) on Windows

eumagga0x2a

Quote from: CoSpi on August 22, 2022, 09:46:33 PMFirst problem is that segments are saved to project file with offset.
For example when I cut middle part of video, 2 segments are created, but first segment doesn't start on 0 pts but 125125 (3 frames offset) in one video file and 83917 (2 frames offset) in other video file.

When a video file is loaded, the start time in reference (the offset of the segment start vs start of the source video) is automatically set to the PTS of the first keyframe. The PTS of the first keyframe can be zero only if the video doesn't contain B-frames. When you create a segment layout by adm.addSegment(int,double,double) where adm = Avidemux(), you can set start time in reference to zero (this was the old behaviour of Avidemux, responsible for "video not starting at zero").

Quote from: CoSpi on August 22, 2022, 09:46:33 PMSecond problem is that I can't determinate if segment starts and ends on I-frame if the project contains any segments.

You could use a combination of adm.setCurrentPts(double) and ed.getCurrentFlags() where ed = Editor() or seek by x keyframes from the current position by adm.seekKeyFrame(x)

Quote from: CoSpi on August 22, 2022, 09:46:33 PMThird problem: segment is not saved when mode is not copy but encode and markers are set with script not to I-frames. File contains only video file heather.

I need to recheck this, will try to do it tomorrow. I could imagine that things can go wrong when the A marker doesn't match any frame at all, but the flags of the frame in the source video should not have any effect on re-encoding.

Quote from: CoSpi on August 22, 2022, 09:46:33 PMOk so I just need to use adm.setCurrentPts(pts) to move to marker A, but adm.setCurrentPts(pts) moves to not to given pts, but to I-frame close to given pts.

Works as designed in case pts doesn't match a frame. I would expect that looping adm.seekFrame(1) until ed.getCurrentPts() is close to the target could help.

CoSpi

I didn't get you answer about the offset, but I find the way how to get it in the script.
I also find the problem with the error after using adm.clearSegments().
I used just adm.clearSegments(), but I didn't add any segment after.
When I looked at generated project file, there is line with adding one segment containing whole video file, but not starting from zero, but with an offset.
I tryed to add the one segment starting with zero, because I don't know the offset, and it works. The offset is added anyway, so I can read it and use it for calculations.

adm = Avidemux()
ed = Editor()
adm.clearSegments()
adm.addSegment(0, 0, ed.getVideoDuration())
print(ed.getTimeOffsetForSegment(0))

CoSpi

Heh the code I wrote doesn't work. ed.getVideoDuration() returns sum of segments durations, so if I call adm.clearSegments() before, it returns 0.
It is similar problem like using pts from segments in adm.setCurrentPts(pts) func.
When segments doesn't contains whole video file, the ed.getVideoDuration() doesn't return duration of video, but just sum of segments.

Lets say I have 10 min video and I cut out part from 1-9 min. 2 segments are created (0-1 min and 9-10 min).
ed.getVideoDuration() will return 2 min and adm.setCurrentPts(ed.getTimeOffsetForSegment(1)) will not work, because I have now just 2 min long video, so I can't move to 9 min.
Thats why I need to get the segments offsets (0-1, 9-10), clear the segments, create one segment containing whole file and than I can set markers to 9-10, test it for I-frames and save it.

I use for testing I-frames this function:
def isKeyframe(pts):
  return pts == ed.getPrevKFramePts(pts+1)

CoSpi

I just found the other function to get video duration getRefVideoDuration() :)
And also getPts() function, which I don't know what it does, but it gives me the offset what I am looking for with parameter 0 :D

CoSpi

I just maybe found the way how to trick the spam filter.
I couldn't post replies, maybe because of parentheses in the text, so I just post some short text and than edit it with real text.

CoSpi

So I managed to get it working.
adm = Avidemux()
ed = Editor()

def isKeyframe(pts):
  return pts == ed.getPrevKFramePts(pts+1)

def getSegments():
  segments = []
  for idx in range(ed.nbSegments()):
    segments.append([
      ed.getTimeOffsetForSegment(idx),
      ed.getDurationForSegment(idx)])
  return segments

def addSegments(segments):
  for segment in segments:
    adm.addSegment(0, segment[0], segment[1])

def resetEdit():
  adm.clearSegments()
  adm.addSegment(0, 0, ed.getRefVideoDuration(0))

def getOffset():
  return ed.getPts(0)

def getFileName():
  return splitext(ed.getRefVideoName(0))[0].split("/")[-1]

def exportSegments(segments, offset, outDir, fileName):
  idx = 0
  zero = "0"

  for segment in segments:
    adm.markerA = segment[0] - offset
    adm.markerB = adm.markerA + segment[1]

    if isKeyframe(adm.markerA) and isKeyframe(adm.markerB):
      adm.audioCodec(0, "copy")
      adm.videoCodec("copy")
      adm.markerB = adm.markerB - 1
    else:
      adm.audioCodec(0, "LavAAC", "bitrate=128")
      adm.videoCodecSetProfile("x264", "x264Default27")

    idx += 1
    if idx > 9: zero = ""
    outFilePath = outDir + fileName + "_clip_" + zero + str(idx) + ".mp4"
    adm.save(outFilePath)
   

segments = getSegments()
resetEdit()
exportSegments(segments, getOffset(), "d:\\avidemux\\", getFileName())

#restore original segments
adm.clearSegments()
addSegments(segments)

eumagga0x2a

Quote from: CoSpi on August 23, 2022, 08:09:13 AMI didn't get you answer about the offset

If you worked with Avidemux versions < 2.8.0, you probably noticed that Avidemux showed for many videos (more precisely, all videos with B-frames and valid timestamps) the first frame with a non-zero PTS. The technical reason was (and is) that Avidemux uses an unsigned integer value (uint64_t) to store PTS and DTS (decode timestamps) which cannot represent negative values. Therefore, the PTS of the first keyframe has to be greater than zero to avoid DTS going negative if there are B-frames and thus at least two frames must be decoded before the first frame can be displayed.

This caused countless support requests as it resulted in timing being shifted in comparison with one displayed e.g. by FFmpeg.

Short time before the 2.8.0 release, the strategy for automatically creating a segment for a loaded or appended video was changed to hide the B-frame-related delay from users by aligning the start of segment with the PTS of the first keyframe, making all videos "start at zero" like they (mostly) do in FFmpeg without rewriting the application.

BTW, a nice script!

CoSpi

New version
I changed the output file names to contain start time of the clip instead of counter.

#PY  <- Needed to identify #
# Script for exporting segments using copy codec if possible
# Martin Holý 2022

adm = Avidemux()
ed = Editor()

def isKeyframe(pts):
  return pts == ed.getPrevKFramePts(pts+1)

def getSegments():
  segments = []
  for idx in range(ed.nbSegments()):
    segments.append([
      ed.getTimeOffsetForSegment(idx),
      ed.getDurationForSegment(idx)])
  return segments

def addSegments(segments):
  for segment in segments:
    adm.addSegment(0, segment[0], segment[1])

def resetEdit():
  adm.clearSegments()
  adm.addSegment(0, 0, ed.getRefVideoDuration(0))

def getOffset():
  return ed.getPts(0)

def getFileName():
  return splitext(ed.getRefVideoName(0))[0].split("/")[-1]

def ptsToStr(pts, hours=True, intSec=True):
  s = pts / 1000000
  h = int(s / 3600)
  s -= h * 3600
  m = int(s / 60)
  s -= m * 60
  sm = str(m)

  if intSec:
    s = round(s)
    ss = str(int(s))
  else:
    # little tinypy limitations walk around
    a = int(s)
    b = int((s - a) * 100)
    s = round(s * 100) / 100
    ss = str(a) + "." + str(b)

  if (m < 10): sm = "0" + sm
  if (s < 10): ss = "0" + ss

  if hours:
    return str(h) + "-" + sm + "-" + ss
  else:
    return sm + "-" + ss

def appendTimeIn(segments, offset):
  hours = (segments[-1][0] - offset) / 1000000.0 / 3600 >= 1
  dict1 = {}

  for segment in segments:
    timeIn = ptsToStr(segment[0] - offset, hours, True)
    segment.append(timeIn)

    if timeIn in dict1:
      dict1[timeIn].append(segment)
    else:
      dict1[timeIn] = [segment]

  for k in dict1:
    if len(dict1[k]) > 1:
      for segment in dict1[k]:
        segment[2] = ptsToStr(segment[0] - offset, hours, False)

def exportSegments(segments, offset, outDir, fileName):
  for segment in segments:
    adm.markerA = segment[0] - offset
    adm.markerB = adm.markerA + segment[1]

    if isKeyframe(adm.markerA) and isKeyframe(adm.markerB):
      adm.audioCodec(0, "copy")
      adm.videoCodec("copy")
      adm.markerB = adm.markerB - 1
    else:
      adm.audioCodec(0, "LavAAC", "bitrate=128")
      adm.videoCodecSetProfile("x264", "x264VerySlow25")

    outFilePath = outDir + fileName + "_" + segment[2] + ".mp4"
    adm.save(outFilePath)

offset = getOffset()
segments = getSegments()
appendTimeIn(segments, offset)
resetEdit()
exportSegments(segments, offset, "d:\\avidemux\\", getFileName())

#restore original segments
adm.clearSegments()
addSegments(segments)

CoSpi

#PY  <- Needed to identify #
# Script for exporting segments using copy codec if possible
# Martin Holý 2022

# 2022.09.15
# - bug fix: using copy codec for segment which starts on key frame and ends on end of the video
# - bug fix: time in round bug (seconds or minutes > 59)
# - add: decode time taken from file name (YYMMDD_HHmmss) and append time in of segment to it

def lstrip(s, x):
  found = True
  out = ""

  for c in s:
    if found:
      if c == x:
        continue
      else:
        found = False
    out += c
 
  return out

def substring(string, start, stop):
  len = stop - start
  out = ""
  i = 0

  for c in string[start:]:
    if i == len : break
    out += c
    i += 1

  return out

adm = Avidemux()
ed = Editor()

def isKeyframe(pts):
  return pts == ed.getPrevKFramePts(pts+1)

def getSegments():
  segments = []
  for idx in range(ed.nbSegments()):
    segments.append([
      ed.getTimeOffsetForSegment(idx),
      ed.getDurationForSegment(idx)])
  return segments

def addSegments(segments):
  for segment in segments:
    adm.addSegment(0, segment[0], segment[1])

def resetEdit():
  adm.clearSegments()
  adm.addSegment(0, 0, ed.getRefVideoDuration(0))

def getOffset():
  return ed.getPts(0)

def getFileName():
  return splitext(ed.getRefVideoName(0))[0].split("/")[-1]

def ptsToStr(pts, sep, hours=True, intSec=True):
  s = pts / 1000000
  h = int(s / 3600)
  s -= h * 3600
  m = int(s / 60)
  s -= m * 60

  if intSec:
    s = round(s)
    if s == 60:
      m += 1
      s = 0
      if m == 60:
        h += 1
        m = 0
    ss = str(int(s))
  else:
    # little tinypy limitations walk around
    a = int(s)
    b = int((s - a) * 100)
    s = round(s * 100) / 100
    ss = str(a) + "." + str(b)

  sh = str(h)
  sm = str(m)
  if (h < 10): sh = "0" + sh
  if (m < 10): sm = "0" + sm
  if (s < 10): ss = "0" + ss

  if hours:
    return sh + sep + sm + sep + ss
  else:
    return sm + sep + ss

def timeTakenToPts(timeTaken):
  h = int(lstrip(substring(timeTaken, 0, 2), "0"))
  m = int(lstrip(substring(timeTaken, 2, 4), "0"))
  s = int(lstrip(substring(timeTaken, 4, 6), "0"))
  return (h * 3600 + m * 60 + s) * 1000000

def appendFileName(segments, offset, fileName, sep, timeTakenPts=0):
  hours = timeTakenPts > 0 or (segments[-1][0] - offset) / 1000000.0 / 3600 >= 1
  dict1 = {}

  for segment in segments:
    timeIn = ptsToStr(segment[0] - offset + timeTakenPts, sep, hours, True)
    segment.append(fileName + "_" + timeIn)

    if timeIn in dict1:
      dict1[timeIn].append(segment)
    else:
      dict1[timeIn] = [segment]

  for k in dict1:
    if len(dict1[k]) > 1:
      for segment in dict1[k]:
        segment[2] = fileName + "_" + ptsToStr(segment[0] - offset + timeTakenPts, sep, hours, False)

def exportSegments(segments, offset, outDir):
  vidDur = ed.getRefVideoDuration(0)

  for segment in segments:
    adm.markerA = segment[0] - offset
    adm.markerB = adm.markerA + segment[1]

    if isKeyframe(adm.markerA) and isKeyframe(adm.markerB) or adm.markerB == vidDur:
      adm.audioCodec(0, "copy")
      adm.videoCodec("copy")
      adm.markerB = adm.markerB - 1
    else:
      adm.audioCodec(0, "LavAAC", "bitrate=128")
      adm.videoCodecSetProfile("x264", "x264VerySlow25")

    outFilePath = outDir + segment[2] + ".mp4"
    adm.save(outFilePath)

def main():
  offset = getOffset()
  segments = getSegments()

  decodeTimeTaken = DFToggle("Decode time taken from file name")
  decodeTimeTaken.value = False
  dialog = DialogFactory("Export Segments")
  dialog.addControl(decodeTimeTaken)

  if dialog.show() == 1 and decodeTimeTaken.value:
    # decode time taken from file name (YYMMDD_HHmmss) and append time in of segment to it
    fileName = getFileName()
    dateTaken = substring(fileName, 0, 8)
    timeTaken = substring(fileName, 9, 15)

    timeTakenPts = timeTakenToPts(timeTaken)
    print(timeTaken)
    appendFileName(segments, offset, dateTaken, "", timeTakenPts)
  else:
    # append time in of segment behind file name
    appendFileName(segments, offset, getFileName(), "-")

  resetEdit()
  exportSegments(segments, offset, "d:\\avidemux\\")

  #restore original segments
  adm.clearSegments()
  addSegments(segments)

main()