@ngx-telly/plugin-timecode
Version:
Telly - SMPTE timecode plugin
204 lines (194 loc) • 11.7 kB
JavaScript
import { AsyncPipe } from '@angular/common';
import * as i0 from '@angular/core';
import { inject, Input, Directive, EventEmitter, Output, ChangeDetectionStrategy, Component, HostListener, HostBinding } from '@angular/core';
import { PlayerComponent, ScrubBarComponent } from '@ngx-telly/player';
import { TellyMediaService, TellyEventsService } from '@ngx-telly/player/services';
import { map, distinctUntilChanged, combineLatest, timer } from 'rxjs';
const secondsToTimecode = (seconds, framerate) => {
let frames = Math.round(seconds * framerate);
const dropFrame = framerate % 1 !== 0;
const delimiter = dropFrame ? ';' : ':';
if (dropFrame) {
const df = framerate <= 30 ? 2 : 4;
const dfDiv2 = df >> 1;
const framesPer10Min = 17982 * dfDiv2;
const framesPer1Min = 1798 * dfDiv2;
const div = Math.floor(frames / framesPer10Min);
let mod = frames % framesPer10Min;
if (mod < df)
mod = mod + df;
frames += 9 * df * div + df * Math.floor((mod - df) / framesPer1Min);
}
const fps = Math.round(framerate);
const totalSeconds = Math.floor(frames / fps);
const h = Math.floor(totalSeconds / 3600) % 24;
const m = Math.floor(totalSeconds / 60) % 60;
const s = totalSeconds % 60;
const f = Math.trunc(frames % fps);
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s
.toString()
.padStart(2, '0')}${delimiter}${f.toString().padStart(2, '0')}`;
};
class TellyTimecodeDirective {
constructor() {
this.fps = 24;
this.player = inject(PlayerComponent);
this.mediaService = inject(TellyMediaService);
this.interval = 0;
this.step = (sec) => {
this.player.media.pause(true);
if (this.player.media.windowTime <= 0) {
this.player.media.seek(Math.abs(sec) / 20);
}
const nextPos = this.player.media.windowTime + sec;
const closestFrame = Math.round(nextPos / this.interval) * this.interval;
this.player.media.seek(closestFrame + this.interval / 4);
this.player.media.indicate(`Step ${sec < 0 ? 'back' : 'forward'}`);
};
this.getTimeCode = () => {
const { media, timeOffset } = this.player;
return secondsToTimecode(media.time + timeOffset(), this.fps);
};
}
ngOnInit() {
if (!this.fps)
throw new Error('FPS not set, cannot evaluate frame interval!');
this.interval = 1 / this.fps;
this.registerFactory();
}
ngOnChanges(changes) {
if (changes['fps']?.currentValue && !changes['fps']?.firstChange) {
this.interval = 1 / changes['fps']?.currentValue;
this.registerFactory();
}
}
registerFactory() {
this.mediaService.registerFactory('TellyTimecode', (media) => {
Object.assign(media, {
timecode: this.getTimeCode,
step: this.step,
fps: this.fps,
stepInterval: this.interval,
});
return media;
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: TellyTimecodeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.16", type: TellyTimecodeDirective, isStandalone: true, selector: "[tellyTimecode]", inputs: { fps: ["tellyTimecode", "fps"] }, usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: TellyTimecodeDirective, decorators: [{
type: Directive,
args: [{
selector: '[tellyTimecode]',
standalone: true,
}]
}], propDecorators: { fps: [{
type: Input,
args: ['tellyTimecode']
}] } });
class TimecodeComponent {
constructor() {
this.timeClicked = new EventEmitter();
this.player = inject(PlayerComponent);
this.directive = inject(TellyTimecodeDirective);
this.events = inject(TellyEventsService);
}
ngOnInit() {
this.time$ = this.events.timer$.pipe(map(() => this.player.media.time + this.player.timeOffset()), distinctUntilChanged(), map((sec) => this.round(sec)), map((sec) => secondsToTimecode(sec, this.directive.fps)));
}
onTimeClick() {
const { media, timeOffset } = this.player;
this.timeClicked.emit(secondsToTimecode(this.round(media.time + timeOffset()), this.directive.fps));
}
round(sec) {
return Math.round(sec * 100) / 100;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: TimecodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.16", type: TimecodeComponent, isStandalone: true, selector: "telly-timecode", outputs: { timeClicked: "timeClicked" }, ngImport: i0, template: `<span (click)="onTimeClick()">{{ time$ | async }}</span>`, isInline: true, styles: [":host{padding-inline:5px;-webkit-user-select:none;user-select:none}\n"], dependencies: [{ kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: TimecodeComponent, decorators: [{
type: Component,
args: [{ selector: 'telly-timecode', template: `<span (click)="onTimeClick()">{{ time$ | async }}</span>`, changeDetection: ChangeDetectionStrategy.OnPush, imports: [AsyncPipe], styles: [":host{padding-inline:5px;-webkit-user-select:none;user-select:none}\n"] }]
}], propDecorators: { timeClicked: [{
type: Output
}] } });
const stepForward = 'M16,18H18V6H16M6,18L14.5,12L6,6V18Z';
const stepBackward = 'M6,18V6H8V18H6M9.5,12L18,6V18L9.5,12Z';
class StepBackComponent {
constructor() {
this.icon = stepBackward;
this.size = '1.6rem';
this.directive = inject(TellyTimecodeDirective);
this.title = 'Step back';
}
onClick() {
this.directive.step(-this.directive.interval);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StepBackComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.16", type: StepBackComponent, isStandalone: true, selector: "telly-step-back", inputs: { icon: "icon", size: "size" }, host: { listeners: { "pointerdown": "onClick()" }, properties: { "attr.tip": "this.title" } }, ngImport: i0, template: "<svg\n viewBox=\"0 0 24 24\"\n [style.width]=\"size\"\n>\n <path\n fill=\"white\"\n [attr.d]=\"icon\"\n />\n</svg>\n", styles: [""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StepBackComponent, decorators: [{
type: Component,
args: [{ selector: 'telly-step-back', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg\n viewBox=\"0 0 24 24\"\n [style.width]=\"size\"\n>\n <path\n fill=\"white\"\n [attr.d]=\"icon\"\n />\n</svg>\n" }]
}], propDecorators: { icon: [{
type: Input
}], size: [{
type: Input
}], title: [{
type: HostBinding,
args: ['attr.tip']
}], onClick: [{
type: HostListener,
args: ['pointerdown']
}] } });
class StepForwardComponent {
constructor() {
this.icon = stepForward;
this.size = '1.6rem';
this.directive = inject(TellyTimecodeDirective);
this.title = 'Step forward';
}
onClick() {
this.directive.step(this.directive.interval);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StepForwardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.16", type: StepForwardComponent, isStandalone: true, selector: "telly-step-forward", inputs: { icon: "icon", size: "size" }, host: { listeners: { "pointerdown": "onClick()" }, properties: { "attr.tip": "this.title" } }, ngImport: i0, template: "<svg\n viewBox=\"0 0 24 24\"\n [style.width]=\"size\"\n>\n <path\n fill=\"white\"\n [attr.d]=\"icon\"\n />\n</svg>\n", styles: [""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StepForwardComponent, decorators: [{
type: Component,
args: [{ selector: 'telly-step-forward', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg\n viewBox=\"0 0 24 24\"\n [style.width]=\"size\"\n>\n <path\n fill=\"white\"\n [attr.d]=\"icon\"\n />\n</svg>\n" }]
}], propDecorators: { icon: [{
type: Input
}], size: [{
type: Input
}], title: [{
type: HostBinding,
args: ['attr.tip']
}], onClick: [{
type: HostListener,
args: ['pointerdown']
}] } });
class ScrubBarTimecodeComponent {
constructor() {
this.scrubBar = inject(ScrubBarComponent);
this.directive = inject(TellyTimecodeDirective);
this.player = inject(PlayerComponent);
this.time$ = combineLatest([this.scrubBar.offset$, timer(0, 500)]).pipe(map(([offset]) => {
const { media, timeOffset } = this.player;
const percentage = offset / this.scrubBar.ref.nativeElement.clientWidth;
const normalized = Math.max(0, Math.min(1, percentage));
return secondsToTimecode(timeOffset() + media.drift + (media.duration || 0) * normalized, this.directive.fps);
}));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: ScrubBarTimecodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.16", type: ScrubBarTimecodeComponent, isStandalone: true, selector: "telly-scrub-bar-timecode", ngImport: i0, template: "<ng-content></ng-content>\n<div>\n {{ time$ | async }}\n</div>\n", styles: ["div{text-align:center}\n"], dependencies: [{ kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: ScrubBarTimecodeComponent, decorators: [{
type: Component,
args: [{ selector: 'telly-scrub-bar-timecode', changeDetection: ChangeDetectionStrategy.OnPush, imports: [AsyncPipe], template: "<ng-content></ng-content>\n<div>\n {{ time$ | async }}\n</div>\n", styles: ["div{text-align:center}\n"] }]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { ScrubBarTimecodeComponent, StepBackComponent, StepForwardComponent, TellyTimecodeDirective, TimecodeComponent, secondsToTimecode };
//# sourceMappingURL=ngx-telly-plugin-timecode.mjs.map