You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lossless-cut/src/segments.js

159 lines
5.2 KiB
JavaScript

import { v4 as uuidv4 } from 'uuid';
import sortBy from 'lodash/sortBy';
6 years ago
export const isDurationValid = (duration) => Number.isFinite(duration) && duration > 0;
export const createSegment = ({ start, end, name, tags, segColorIndex } = {}) => ({
6 years ago
start,
end,
name: name || '',
segId: uuidv4(),
segColorIndex,
// `tags` is an optional object (key-value). Values must always be string
// See https://github.com/mifi/lossless-cut/issues/879
tags: tags != null && typeof tags === 'object'
? Object.fromEntries(Object.entries(tags).map(([key, value]) => [key, String(value)]))
: undefined,
6 years ago
});
// Because segments could have undefined start / end
// (meaning extend to start of timeline or end duration)
export function getSegApparentStart(seg) {
const time = seg.start;
return time !== undefined ? time : 0;
}
export function getSegApparentEnd(seg, duration) {
const time = seg.end;
if (time !== undefined) return time;
if (isDurationValid(duration)) return duration;
return 0; // Haven't gotten duration yet - what do to ¯\_(ツ)_/¯
}
6 years ago
export const getCleanCutSegments = (cs) => cs.map((seg) => ({
start: seg.start,
end: seg.end,
name: seg.name,
tags: seg.tags,
6 years ago
}));
export function findSegmentsAtCursor(apparentSegments, currentTime) {
const indexes = [];
apparentSegments.forEach((segment, index) => {
if (segment.start < currentTime && segment.end > currentTime) indexes.push(index);
});
return indexes;
}
export const getSegmentTags = (segment) => (segment.tags || {});
export const sortSegments = (segments) => sortBy(segments, 'start');
// https://stackoverflow.com/a/30472982/6519037
export function partitionIntoOverlappingRanges(array, getSegmentStart = (seg) => seg.start, getSegmentEnd = (seg) => seg.end) {
function getMaxEnd(array2) {
// note: this also mutates array2
array2.sort((a, b) => {
if (getSegmentEnd(a) < getSegmentEnd(b)) return 1;
if (getSegmentEnd(a) > getSegmentEnd(b)) return -1;
return 0;
});
return getSegmentEnd(array2[0]);
}
const ret = [];
let g = 0;
ret[g] = [array[0]];
for (let i = 1; i < array.length; i += 1) {
if (getSegmentStart(array[i]) >= getSegmentStart(array[i - 1]) && getSegmentStart(array[i]) < getMaxEnd(ret[g])) {
ret[g].push(array[i]);
} else {
g += 1;
ret[g] = [array[i]];
}
}
return ret.filter((group) => group.length > 1).map((group) => sortBy(group, (seg) => getSegmentStart(seg)));
}
export function combineOverlappingSegments(existingSegments, getSegApparentEnd2) {
const partitionedSegments = partitionIntoOverlappingRanges(existingSegments, getSegApparentStart, getSegApparentEnd2);
return existingSegments.map((existingSegment) => {
const partOfPartition = partitionedSegments.find((partition) => partition.includes(existingSegment));
if (partOfPartition == null) return existingSegment; // this is not an overlapping segment, pass it through
const index = partOfPartition.indexOf(existingSegment);
// The first segment is the one with the lowest "start" value, so we use its start value
if (index === 0) {
return {
...existingSegment,
// but use the segment with the highest "end" value as the end value.
end: sortBy(partOfPartition, (segment) => segment.end)[partOfPartition.length - 1].end,
};
}
return undefined; // then remove all other segments in this partition group
}).filter((segment) => segment);
}
export function hasAnySegmentOverlap(sortedSegments) {
if (sortedSegments.length < 1) return false;
const overlappingGroups = partitionIntoOverlappingRanges(sortedSegments);
return overlappingGroups.length > 0;
}
export function invertSegments(sortedCutSegments, includeFirstSegment, includeLastSegment, duration) {
if (sortedCutSegments.length < 1) return undefined;
if (hasAnySegmentOverlap(sortedCutSegments)) return undefined;
const ret = [];
if (includeFirstSegment) {
if (sortedCutSegments[0].start > 0) {
ret.push({
start: 0,
end: sortedCutSegments[0].start,
});
}
}
sortedCutSegments.forEach((cutSegment, i) => {
if (i === 0) return;
ret.push({
start: sortedCutSegments[i - 1].end,
end: cutSegment.start,
});
});
if (includeLastSegment) {
const last = sortedCutSegments[sortedCutSegments.length - 1];
if (last.end < duration || duration == null) {
ret.push({
start: last.end,
end: duration,
});
}
}
// Filter out zero length resulting segments
// https://github.com/mifi/lossless-cut/issues/909
5 years ago
return ret.filter(({ start, end }) => end == null || start == null || end > start);
}
// because chapters need to be contiguous, we need to insert gaps in-between
export function convertSegmentsToChapters(sortedSegments) {
if (sortedSegments.length < 1) return [];
if (hasAnySegmentOverlap(sortedSegments)) throw new Error('Segments cannot overlap');
sortedSegments.map((segment) => ({ start: segment.start, end: segment.end, name: segment.name }));
const invertedSegments = invertSegments(sortedSegments, true, false);
// inverted segments will be "gap" segments. Merge together with normal segments
return sortSegments([...sortedSegments, ...invertedSegments]);
}