Skip to content

Commit

Permalink
Merge pull request #11 from Joery-M/1-shared-implement-timeline-and-t…
Browse files Browse the repository at this point in the history
…imelineitem

1 shared implement timeline and timelineitem
  • Loading branch information
Joery-M authored Apr 19, 2024
2 parents 71987f2 + 11244e5 commit d876dd6
Show file tree
Hide file tree
Showing 11 changed files with 331 additions and 37 deletions.
4 changes: 3 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss"
"bradlc.vscode-tailwindcss",
"vivaxy.vscode-conventional-commits",
"vue.volar"
]
}
Empty file.
61 changes: 59 additions & 2 deletions packages/shared/src/Media/Media.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { type MediaInfoType } from 'mediainfo.js';
import type { DateTime } from 'luxon';
import type { MediaInfoType } from 'mediainfo.js';
import { ref } from 'vue';
import MissingThumbnailUrl from '../../assets/missing_thumbnail.png?url';
import type { DateTime } from 'luxon';
import type { MaybePromiseResult } from '../../types/MaybePromise';
import type BaseTimelineItem from '../base/TimelineItem';
import AudioTimelineItem from '../TimelineItem/AudioTimelineItem';
import VideoTimelineItem from '../TimelineItem/VideoTimelineItem';

// Not sure if refs are needed here, might want to look at this in the future.

Expand Down Expand Up @@ -53,6 +57,55 @@ export default class Media {

return (this.type & totalType) == totalType;
}

static timelineItemCreators: MediaToTimelineItemFunc[] = [mediaToVideoTimelineItem];

async createTimelineItems<T extends BaseTimelineItem>(): Promise<T[] | undefined> {
const results = (await Promise.all(Media.timelineItemCreators.map((f) => f(this))))
.filter((i) => i !== undefined)
.flat(1);

// Link all items together so they move with each other
results.forEach((item) => {
results.forEach((itemToLink) => {
if (item !== itemToLink) {
item.linkedItems.add(itemToLink);
}
});
});

return results as T[];
}
}

function mediaToVideoTimelineItem(media: Media) {
const allItems = [] as (VideoTimelineItem | AudioTimelineItem)[];
if (media.isOfType(MediaType.Video)) {
const videoItems = media.videoTracks.map((track, i) => {
const tItem = new VideoTimelineItem();
tItem.media.value = media;
tItem.trackInfo.value = track;
tItem.layer.value = media.audioTracks.length + i;
tItem.duration.value = track.duration;
return tItem;
});

allItems.push(...videoItems);
}
if (media.isOfType(MediaType.Audio)) {
const audioItems = media.audioTracks.map((track, i) => {
const tItem = new AudioTimelineItem();
tItem.media.value = media;
tItem.trackInfo.value = track;
tItem.layer.value = i;
tItem.duration.value = track.duration;
return tItem;
});

allItems.push(...audioItems);
}

return allItems;
}

export enum MediaType {
Expand Down Expand Up @@ -105,3 +158,7 @@ export interface ImageInfo {
export interface TextTrackInfo {
format: string;
}

export type MediaToTimelineItemFunc = (
media: Media
) => MaybePromiseResult<BaseTimelineItem[] | undefined>;
27 changes: 25 additions & 2 deletions packages/shared/src/Project/SimpleProject.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import SimpleTimeline from '../Timeline/SimpleTimeline';
import { v4 as uuidv4 } from 'uuid';
import { computed, ref, shallowReactive } from 'vue';
import { default as BaseProject, type ProjectType } from '../base/Project';
import BaseProject, { type ProjectType } from '../base/Project';
import Media from '../Media/Media';
import SimpleTimeline, { type SimpleTimelineConfig } from '../Timeline/SimpleTimeline';

export default class SimpleProject extends BaseProject {
public id = uuidv4();
Expand All @@ -14,4 +14,27 @@ export default class SimpleProject extends BaseProject {
public selectedTimelineIndex = ref(0);
public timelines = shallowReactive<SimpleTimeline[]>([]);
public timeline = computed(() => this.timelines.at(this.selectedTimelineIndex.value)!);

public usesMedia(media: Media) {
return this.timelines.some((timeline) => timeline.usesMedia(media));
}

public selectTimeline(timeline: SimpleTimeline) {
const timelineIndex = this.timelines.indexOf(timeline);
if (timelineIndex >= 0) {
this.selectedTimelineIndex.value = timelineIndex;
}
}

public createTimeline(config: SimpleTimelineConfig, selectWhenCreated = true): SimpleTimeline {
const timeline = new SimpleTimeline(config);

this.timelines.push(timeline);

if (selectWhenCreated) {
this.selectTimeline(timeline);
}

return timeline;
}
}
67 changes: 54 additions & 13 deletions packages/shared/src/Timeline/SimpleTimeline.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,68 @@
import { v4 as uuidv4 } from 'uuid';
import { shallowReactive } from 'vue';
import { computed, ref, shallowReactive } from 'vue';
import BaseTimeline, { type TimelineType } from '../base/Timeline';
import type BaseTimelineItem from '../base/TimelineItem';
import type Media from '../Media/Media';

export default class SimpleTimeline extends BaseTimeline {
public name = 'Untitled';
public name = ref('Untitled');
public id = uuidv4();
public type: TimelineType = 'Simple';

public items = shallowReactive<BaseTimelineItem[]>([]);
public items = shallowReactive(new Set<BaseTimelineItem>());

public duration = 0;
public viewportWidth = 1920;
public viewportWeight = 1080;
public framerate = 60;
public width = ref(1920);
public height = ref(1080);
public framerate = ref(60);
/**
* Duration of a single frame in milliseconds
*/
public frameDuration = computed(() => {
return 1000 / this.framerate.value;
});

constructor() {
constructor(config: SimpleTimelineConfig) {
super();
this.duration = 0;
this.width = 1920;
this.height = 1080;
this.framerate = 60;
if (config.name) {
this.name.value = config.name;
}
this.width.value = config.width;
this.height.value = config.height;
this.framerate.value = config.framerate;
}

updateDuration() {}
/**
* Called when an item is dropped in the timeline
*/
public itemDropped(otherItem: BaseTimelineItem) {
this.items.forEach((item) => {
if (
item.layer.value === otherItem.layer.value &&
item.end.value > otherItem.start.value &&
item.start.value < otherItem.end.value &&
item.id !== otherItem.id
) {
item.onDroppedOn(otherItem);
}
});
}

public usesMedia(media: Media) {
for (const item of this.items) {
return item.hasMedia() && item.media.value == media;
}
return false;
}

public deleteItem(item: BaseTimelineItem) {
item.Delete();
return this.items.delete(item);
}
}

export interface SimpleTimelineConfig {
name?: string;
width: number;
height: number;
framerate: number;
}
20 changes: 17 additions & 3 deletions packages/shared/src/TimelineItem/AudioTimelineItem.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import { shallowRef } from 'vue';
import Media from '../Media/Media';
import { ref, shallowRef } from 'vue';
import Media, { type AudioTrackInfo } from '../Media/Media';
import BaseTimelineItem, { type TimelineItemType } from '../base/TimelineItem';
import type { TimelineItemMedia } from './interfaces';

export default class AudioTimelineItem extends BaseTimelineItem {
export default class AudioTimelineItem extends BaseTimelineItem implements TimelineItemMedia {
public type: TimelineItemType = 'Audio';
public media = shallowRef<Media>();

/**
* Offset from the start of this timeline item to the start of the audio track.
*/
public startOffset = ref(0);
/**
* Total duration of this audio track.
*/
public duration = ref(0);

public trackInfo = ref<AudioTrackInfo>();

hasMedia = (): this is typeof this & TimelineItemMedia => true;
}
25 changes: 21 additions & 4 deletions packages/shared/src/TimelineItem/VideoTimelineItem.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import { shallowRef } from 'vue';
import Media from '../Media/Media';
import { ref, shallowRef } from 'vue';
import Media, { type VideoTrackInfo } from '../Media/Media';
import BaseTimelineItem, { type TimelineItemType } from '../base/TimelineItem';
import type { TimelineItemMedia } from './interfaces';

export default class VideoTimelineItem extends BaseTimelineItem {
export default class VideoTimelineItem extends BaseTimelineItem implements TimelineItemMedia {
public type: TimelineItemType = 'Video';
public media = shallowRef<Media>();

public RenderVideoFrame() {}
/**
* Offset from the start of this timeline item to the start of the video track.
*/
public startOffset = ref(0);
/**
* Total duration of this video track.
*/
public duration = ref(0);

public trackInfo = ref<VideoTrackInfo>();

// TODO: Create a whole extensible rendering engine (lol)
public RenderVideoFrame() {
throw new Error('Not implemented');
}

hasMedia = (): this is typeof this & TimelineItemMedia => true;
}
7 changes: 7 additions & 0 deletions packages/shared/src/TimelineItem/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { ShallowRef } from 'vue';
import type Media from '../Media/Media';

export interface TimelineItemMedia {
media: ShallowRef<Media | undefined>;
hasMedia: () => this is TimelineItemMedia;
}
8 changes: 2 additions & 6 deletions packages/shared/src/base/Timeline.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import type { Ref } from 'vue';
import type SimpleTimeline from '../Timeline/SimpleTimeline';

export default abstract class BaseTimeline {
public abstract name: string;
public abstract name: Ref<string>;
public abstract id: string;
public type: TimelineType = 'Base';

public width = 1920;
public height = 1080;

public abstract duration: number;

isBaseTimeline = (): this is BaseTimeline => this.type == 'Base';
isSimpleTimeline = (): this is SimpleTimeline => this.type == 'Simple';
}
Expand Down
Loading

0 comments on commit d876dd6

Please sign in to comment.