From 21d4251b9bcd5acb0f8a1761bc4edc34a3dbc228 Mon Sep 17 00:00:00 2001 From: Yassine Doghri Date: Tue, 28 Dec 2021 16:59:19 +0000 Subject: [PATCH] feat: add audio-clipper webcomponent (wip) --- app/Resources/js/admin.ts | 1 + app/Resources/js/modules/audio-clipper.ts | 440 ++++++++++++++++++++ package-lock.json | 42 ++ package.json | 2 + themes/cp_admin/episode/video_clips_new.php | 34 +- 5 files changed, 499 insertions(+), 20 deletions(-) create mode 100644 app/Resources/js/modules/audio-clipper.ts diff --git a/app/Resources/js/admin.ts b/app/Resources/js/admin.ts index cfb0ef08..d4c31d3e 100644 --- a/app/Resources/js/admin.ts +++ b/app/Resources/js/admin.ts @@ -1,5 +1,6 @@ import "@github/markdown-toolbar-element"; import "@github/time-elements"; +import "./modules/audio-clipper"; import ClientTimezone from "./modules/ClientTimezone"; import Clipboard from "./modules/Clipboard"; import DateTimePicker from "./modules/DateTimePicker"; diff --git a/app/Resources/js/modules/audio-clipper.ts b/app/Resources/js/modules/audio-clipper.ts new file mode 100644 index 00000000..aed44631 --- /dev/null +++ b/app/Resources/js/modules/audio-clipper.ts @@ -0,0 +1,440 @@ +import { css, html, LitElement, TemplateResult } from "lit"; +import { + customElement, + property, + query, + queryAssignedNodes, + state, +} from "lit/decorators.js"; +import WaveSurfer from "wavesurfer.js"; + +enum ACTIONS { + StretchLeft, + StretchRight, + Seek, +} + +@customElement("audio-clipper") +export class AudioClipper extends LitElement { + @queryAssignedNodes("audio", true) + _audio!: NodeListOf; + + @queryAssignedNodes("start_time", true) + _startTimeInput!: NodeListOf; + + @queryAssignedNodes("duration", true) + _durationInput!: NodeListOf; + + @query(".slider") + _sliderNode!: HTMLDivElement; + + @query(".slider__segment--wrapper") + _segmentNode!: HTMLDivElement; + + @query(".slider__segment-content") + _segmentContentNode!: HTMLDivElement; + + @query(".slider__segment-progress-handle") + _progressNode!: HTMLDivElement; + + @query("#waveform") + _waveformNode!: HTMLDivElement; + + @property({ type: Number, attribute: "start-time" }) + startTime = 0; + + @property({ type: Number }) + duration = 10; + + @property({ type: Number, attribute: "min-duration" }) + minDuration = 5; + + @property({ type: Number, attribute: "volume" }) + initVolume = 0.5; + + @state() + _isPlaying = false; + + @state() + _clip = { + startTime: 0, + endTime: 0, + }; + + @state() + _action: ACTIONS | null = null; + + @state() + _audioDuration = 0; + + @state() + _sliderWidth = 0; + + @state() + _currentTime = 0; + + @state() + _volume = 0.5; + + @state() + _wavesurfer!: WaveSurfer; + + connectedCallback(): void { + super.connectedCallback(); + + console.log("connectedCallback_before"); + this._clip = { + startTime: this.startTime, + endTime: this.startTime + this.duration, + }; + this._volume = this.initVolume; + console.log("connectedCallback_after"); + } + + protected firstUpdated(): void { + console.log("firstUpdate"); + this._audioDuration = this._audio[0].duration; + this._audio[0].volume = this._volume; + + this._wavesurfer = WaveSurfer.create({ + container: this._waveformNode, + interact: false, + barWidth: 2, + barHeight: 1, + responsive: true, + }); + this._wavesurfer.load(this._audio[0].src); + + window.addEventListener("load", () => { + this._sliderWidth = this._sliderNode.clientWidth; + this.setSegmentPosition(); + }); + window.addEventListener("resize", () => { + this._sliderWidth = this._sliderNode.clientWidth; + this.setSegmentPosition(); + }); + + document.addEventListener("mouseup", () => { + if (this._action !== null) { + this._action = null; + } + }); + document.addEventListener("mousemove", (event: MouseEvent) => { + if (this._action !== null) { + this.updatePosition(event); + } + }); + + this._audio[0].addEventListener("play", () => { + this._isPlaying = true; + }); + this._audio[0].addEventListener("pause", () => { + this._isPlaying = false; + }); + // this._audio[0].addEventListener("timeupdate", () => { + // this._currentTime = this._audio[0].currentTime; + // }); + } + + disconnectedCallback(): void { + console.log("disconnectedCallback"); + + window.removeEventListener("load", () => { + this._sliderWidth = this._sliderNode.clientWidth; + this.setSegmentPosition(); + }); + window.removeEventListener("resize", () => { + this._sliderWidth = this._sliderNode.clientWidth; + this.setSegmentPosition(); + }); + + document.removeEventListener("mouseup", () => { + if (this._action !== null) { + this._action = null; + } + }); + document.removeEventListener("mousemove", (event: MouseEvent) => { + if (this._action !== null) { + this.updatePosition(event); + } + }); + + this._audio[0].removeEventListener("play", () => { + this._isPlaying = true; + }); + this._audio[0].removeEventListener("pause", () => { + this._isPlaying = false; + }); + // this._audio[0].removeEventListener("timeupdate", () => { + // this._currentTime = this._audio[0].currentTime; + // }); + } + + setSegmentPosition(): void { + const startTimePosition = this.getPositionFromSeconds(this._clip.startTime); + const endTimePosition = this.getPositionFromSeconds(this._clip.endTime); + + this._segmentNode.style.transform = `translateX(${startTimePosition}px)`; + this._segmentContentNode.style.width = `${ + endTimePosition - startTimePosition + }px`; + } + + getPositionFromSeconds(seconds: number) { + return (seconds * this._sliderWidth) / this._audioDuration; + } + + getSecondsFromPosition(position: number) { + return (this._audioDuration * position) / this._sliderWidth; + } + + protected updated( + _changedProperties: Map + ): void { + // console.log("updated", _changedProperties); + + if (_changedProperties.has("_clip")) { + // console.log("CLIP", _changedProperties.get("_clip")); + this.pause(); + this.setSegmentPosition(); + console.log(this._clip.startTime); + this._audio[0].currentTime = 58; + console.log(this._audio[0].currentTime); + } + } + + play(): void { + this._audio[0].play(); + // setTimeout(() => { + // this.pause(); + // this._audio[0].currentTime = this._clip.startTime; + // }, (this._clip.endTime - this._clip.startTime) * 1000); + } + + pause(): void { + this._audio[0].pause(); + } + + updatePosition(event: MouseEvent): void { + const cursorPosition = + event.clientX - + (this._sliderNode.getBoundingClientRect().left + + document.documentElement.scrollLeft); + + const seconds = this.getSecondsFromPosition(cursorPosition); + + switch (this._action) { + case ACTIONS.StretchLeft: { + let startTime; + if (seconds > 0) { + if (seconds > this._clip.endTime - this.minDuration) { + startTime = this._clip.endTime - this.minDuration; + } else { + startTime = seconds; + } + } else { + startTime = 0; + } + this._clip = { + startTime, + endTime: this._clip.endTime, + }; + break; + } + case ACTIONS.StretchRight: { + let endTime; + if (seconds < this._audioDuration) { + if (seconds < this._clip.startTime + this.minDuration) { + endTime = this._clip.startTime + this.minDuration; + } else { + endTime = seconds; + } + } else { + endTime = this._audioDuration; + } + + this._clip = { + startTime: this._clip.startTime, + endTime, + }; + break; + } + case ACTIONS.Seek: { + console.log("seeking"); + break; + } + default: + break; + } + } + + setVolume(event: InputEvent): void { + this._volume = parseFloat((event.target as HTMLInputElement).value); + this._audio[0].volume = this._volume; + } + + setCurrentTime(event: MouseEvent): void { + const cursorPosition = + event.clientX - + (this._sliderNode.getBoundingClientRect().left + + document.documentElement.scrollLeft); + + const seconds = this.getSecondsFromPosition(cursorPosition); + this._audio[0].currentTime = seconds; + } + + setAction(action: ACTIONS): void { + this._action = action; + } + + secondsToHHMMSS(seconds: number): string { + return new Date(seconds * 1000).toISOString().substr(11, 8); + } + + static styles = css` + .slider { + position: relative; + height: 6rem; + display: flex; + align-items: center; + width: 100%; + background-color: #0f172a; + } + + .slider__track-placeholder { + width: 100%; + height: 8px; + background-color: #64748b; + } + + .slider__segment--wrapper { + position: absolute; + } + + .slider__segment { + position: relative; + display: flex; + } + + .slider__segment-content { + background-color: rgba(255, 255, 255, 0.9); + height: 4rem; + width: 1px; + border: none; + } + + .slider__segment-progress-handle { + position: absolute; + width: 9px; + height: 9px; + margin-top: -9px; + margin-left: -4px; + background-color: #3b82f6; + cursor: pointer; + } + + .slider__segment .slider__segment-handle { + position: absolute; + cursor: pointer; + width: 1rem; + height: 100%; + background-color: #b91c1c; + border: none; + } + + .slider__segment .slider__segment-handle::before { + content: ""; + position: absolute; + height: 3rem; + width: 2px; + background-color: #ffffff; + margin: auto; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + + .slider__segment .clipper__handle-left { + left: -1rem; + border-radius: 0.2rem 0 0 0.2rem; + } + + .slider__segment .clipper__handle-right { + right: -1rem; + border-radius: 0 0.2rem 0.2rem 0; + } + `; + + render(): TemplateResult<1> { + return html` + + + +
${this.secondsToHHMMSS(this._clip.startTime)}
+
${this.secondsToHHMMSS(this._currentTime)}
+
${this.secondsToHHMMSS(this._clip.endTime)}
+ +
+
+
+
+
+ +
+ +
+ +
+
+
+ + `; + } +} diff --git a/package-lock.json b/package-lock.json index cc03f411..aebd3b99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "leaflet.markercluster": "^1.5.3", "lit": "^2.0.2", "marked": "^4.0.7", + "wavesurfer.js": "^5.2.0", "xml-formatter": "^2.5.1" }, "devDependencies": { @@ -43,6 +44,7 @@ "@tailwindcss/typography": "^0.5.0-alpha.3", "@types/leaflet": "^1.7.6", "@types/marked": "^4.0.1", + "@types/wavesurfer.js": "^5.2.2", "@typescript-eslint/eslint-plugin": "^5.7.0", "@typescript-eslint/parser": "^5.7.0", "cross-env": "^7.0.3", @@ -3248,6 +3250,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==", + "dev": true + }, "node_modules/@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", @@ -3342,6 +3350,15 @@ "version": "2.0.2", "license": "MIT" }, + "node_modules/@types/wavesurfer.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/wavesurfer.js/-/wavesurfer.js-5.2.2.tgz", + "integrity": "sha512-/vjpf81co0SK3z4F5V79fZrFPQ8pw9/fEpgkzcgNVkBa9sY0gAaYzKuaQyCX/yjVf6kc73uPtWABQuVgvpguDQ==", + "dev": true, + "dependencies": { + "@types/debounce": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.8.1.tgz", @@ -15895,6 +15912,11 @@ "node": ">=10" } }, + "node_modules/wavesurfer.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-5.2.0.tgz", + "integrity": "sha512-SkPlTXfvKy+ZnEA7f7g7jn6iQg5/8mAvWpVV5vRbIS/FF9TB2ak9J7VayQfzfshOLW/CqccTiN6DDR/fZA902g==" + }, "node_modules/webidl-conversions": { "version": "6.1.0", "license": "BSD-2-Clause", @@ -18897,6 +18919,12 @@ "version": "1.1.1", "dev": true }, + "@types/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==", + "dev": true + }, "@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", @@ -18979,6 +19007,15 @@ "@types/trusted-types": { "version": "2.0.2" }, + "@types/wavesurfer.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/wavesurfer.js/-/wavesurfer.js-5.2.2.tgz", + "integrity": "sha512-/vjpf81co0SK3z4F5V79fZrFPQ8pw9/fEpgkzcgNVkBa9sY0gAaYzKuaQyCX/yjVf6kc73uPtWABQuVgvpguDQ==", + "dev": true, + "requires": { + "@types/debounce": "*" + } + }, "@typescript-eslint/eslint-plugin": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.8.1.tgz", @@ -27308,6 +27345,11 @@ "xml-name-validator": "^3.0.0" } }, + "wavesurfer.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-5.2.0.tgz", + "integrity": "sha512-SkPlTXfvKy+ZnEA7f7g7jn6iQg5/8mAvWpVV5vRbIS/FF9TB2ak9J7VayQfzfshOLW/CqccTiN6DDR/fZA902g==" + }, "webidl-conversions": { "version": "6.1.0" }, diff --git a/package.json b/package.json index 0016ef21..d619d3f9 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "leaflet.markercluster": "^1.5.3", "lit": "^2.0.2", "marked": "^4.0.7", + "wavesurfer.js": "^5.2.0", "xml-formatter": "^2.5.1" }, "devDependencies": { @@ -61,6 +62,7 @@ "@tailwindcss/typography": "^0.5.0-alpha.3", "@types/leaflet": "^1.7.6", "@types/marked": "^4.0.1", + "@types/wavesurfer.js": "^5.2.2", "@typescript-eslint/eslint-plugin": "^5.7.0", "@typescript-eslint/parser": "^5.7.0", "cross-env": "^7.0.3", diff --git a/themes/cp_admin/episode/video_clips_new.php b/themes/cp_admin/episode/video_clips_new.php index 442df6f4..6df761c2 100644 --- a/themes/cp_admin/episode/video_clips_new.php +++ b/themes/cp_admin/episode/video_clips_new.php @@ -10,9 +10,20 @@ section('content') ?> -
+ - +
+ + + + + + +
+ +