UNPKG

@ngx-telly/plugin-timecode

Version:

Telly - SMPTE timecode plugin

204 lines (194 loc) 11.7 kB
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