From 752a27a2adb1aee4721fb28c203c55154312ede5 Mon Sep 17 00:00:00 2001 From: Brandon Martel Date: Fri, 4 Jul 2025 11:35:15 -0500 Subject: [PATCH 01/19] chore: FIT-339: Remove feature flags ff_front_dev_2715_audio_3_280722_short,ff_front_DEV_1713_audio_ui_150222_short --- .../core/feature_flags/stale_feature_flags.py | 1 - label_studio/feature_flags.json | 56 +- .../datamanager/src/utils/feature-flags.js | 6 - .../InteractiveOverlays/BoundingBox.js | 14 +- .../InteractiveOverlays/NodesConnector.js | 10 +- .../SidePanels/DetailsPanel/RegionEditor.tsx | 3 +- .../src/components/Timeline/Controls.tsx | 5 +- .../src/components/Waveform/Waveform.jsx | 578 ------------------ .../components/Waveform/Waveform.module.scss | 16 - .../editor/src/core/feature-flags/flags.json | 2 - web/libs/editor/src/regions/AudioRegion.js | 20 +- .../regions/AudioRegion/AudioRegionModel.js | 318 +++++----- .../AudioRegion/AudioUltraRegionModel.js | 179 ------ web/libs/editor/src/tags/object/Audio.jsx | 133 ---- .../editor/src/tags/object/Audio/Controls.jsx | 38 -- .../object/{AudioNext => Audio}/constants.ts | 0 .../editor/src/tags/object/Audio/index.js | 13 + .../object/{AudioUltra => Audio}/model.js | 0 .../object/{AudioUltra => Audio}/view.scss | 0 .../object/{AudioUltra => Audio}/view.tsx | 12 +- .../editor/src/tags/object/AudioNext/index.js | 29 - .../editor/src/tags/object/AudioNext/model.js | 331 ---------- .../editor/src/tags/object/AudioNext/view.tsx | 135 ---- .../src/tags/object/AudioNext/view_old.jsx | 52 -- .../object/AudioPlus/AudioPlus.module.scss | 9 - .../src/tags/object/AudioUltra/constants.ts | 20 - .../editor/src/tags/object/Video/HtxVideo.jsx | 35 +- web/libs/editor/src/utils/feature-flags.ts | 9 - .../editor/tests/e2e/tests/maxUsage.test.js | 3 - .../editor/tests/integration/feature-flags.ts | 1 - web/libs/frontend-test/src/feature-flags.ts | 9 - web/package.json | 1 - web/yarn.lock | 5 - 33 files changed, 173 insertions(+), 1870 deletions(-) delete mode 100644 web/libs/editor/src/components/Waveform/Waveform.jsx delete mode 100644 web/libs/editor/src/components/Waveform/Waveform.module.scss delete mode 100644 web/libs/editor/src/regions/AudioRegion/AudioUltraRegionModel.js delete mode 100644 web/libs/editor/src/tags/object/Audio.jsx delete mode 100644 web/libs/editor/src/tags/object/Audio/Controls.jsx rename web/libs/editor/src/tags/object/{AudioNext => Audio}/constants.ts (100%) create mode 100644 web/libs/editor/src/tags/object/Audio/index.js rename web/libs/editor/src/tags/object/{AudioUltra => Audio}/model.js (100%) rename web/libs/editor/src/tags/object/{AudioUltra => Audio}/view.scss (100%) rename web/libs/editor/src/tags/object/{AudioUltra => Audio}/view.tsx (96%) delete mode 100644 web/libs/editor/src/tags/object/AudioNext/index.js delete mode 100644 web/libs/editor/src/tags/object/AudioNext/model.js delete mode 100644 web/libs/editor/src/tags/object/AudioNext/view.tsx delete mode 100644 web/libs/editor/src/tags/object/AudioNext/view_old.jsx delete mode 100644 web/libs/editor/src/tags/object/AudioPlus/AudioPlus.module.scss delete mode 100644 web/libs/editor/src/tags/object/AudioUltra/constants.ts diff --git a/label_studio/core/feature_flags/stale_feature_flags.py b/label_studio/core/feature_flags/stale_feature_flags.py index e35e522a0ecf..cf9582e8fa14 100644 --- a/label_studio/core/feature_flags/stale_feature_flags.py +++ b/label_studio/core/feature_flags/stale_feature_flags.py @@ -16,7 +16,6 @@ 'fflag_fix_front_leap_32_zoom_perf_190923_short': True, 'fflag_feat_front_lsdv_5452_taxonomy_labeling_110823_short': True, 'fflag_fix_front_dev_3793_relative_coords_short': True, - 'ff_front_dev_2715_audio_3_280722_short': True, # Feb 5 'fflag_feature_all_optic_1421_cold_start_v2': False, 'fflag_fix_back_optic_1407_optimize_tasks_api_pagination_counts': True, diff --git a/label_studio/feature_flags.json b/label_studio/feature_flags.json index 15da7eedb642..5e659d9beed7 100644 --- a/label_studio/feature_flags.json +++ b/label_studio/feature_flags.json @@ -433,33 +433,7 @@ "version": 4, "deleted": false }, - "ff_front_DEV_1713_audio_ui_150222_short": { - "key": "ff_front_DEV_1713_audio_ui_150222_short", - "on": false, - "prerequisites": [], - "targets": [], - "contextTargets": [], - "rules": [], - "fallthrough": { - "variation": 0 - }, - "offVariation": 1, - "variations": [ - true, - false - ], - "clientSideAvailability": { - "usingMobileKey": false, - "usingEnvironmentId": false - }, - "clientSide": false, - "salt": "b9a9a797e1884581b1ab307952adbb2c", - "trackEvents": false, - "trackEventsFallthrough": false, - "debugEventsUntilDate": null, - "version": 2, - "deleted": false - }, + "ff_front_dev_1442_unselect_shape_on_click_outside_080622_short": { "key": "ff_front_dev_1442_unselect_shape_on_click_outside_080622_short", "on": false, @@ -649,33 +623,7 @@ "version": 2, "deleted": false }, - "ff_front_dev_2715_audio_3_280722_short": { - "key": "ff_front_dev_2715_audio_3_280722_short", - "on": true, - "prerequisites": [], - "targets": [], - "contextTargets": [], - "rules": [], - "fallthrough": { - "variation": 0 - }, - "offVariation": 1, - "variations": [ - true, - false - ], - "clientSideAvailability": { - "usingMobileKey": false, - "usingEnvironmentId": false - }, - "clientSide": false, - "salt": "a8eb8122a118496eab36bacd9dffff6b", - "trackEvents": false, - "trackEventsFallthrough": false, - "debugEventsUntilDate": null, - "version": 4, - "deleted": false - }, + "ff_front_optic_1494_saved_templates_to_custom_templates": { "key": "ff_front_optic_1494_saved_templates_to_custom_templates", "on": true, diff --git a/web/libs/datamanager/src/utils/feature-flags.js b/web/libs/datamanager/src/utils/feature-flags.js index cd46b353374f..d6968bc5d4dd 100644 --- a/web/libs/datamanager/src/utils/feature-flags.js +++ b/web/libs/datamanager/src/utils/feature-flags.js @@ -9,12 +9,6 @@ export const FF_DEV_2186 = "ff_front_dev_2186_comments_for_update"; export const FF_DEV_2536 = "fflag_feat_front_dev-2536_comment_notifications_short"; -/** - * Support for loading media files only a single time. Part of the Audio v3 epic. - * @link https://app.launchdarkly.com/default/production/features/ff_front_dev_2715_audio_3_280722_short - */ -export const FF_DEV_2715 = "ff_front_dev_2715_audio_3_280722_short"; - // Comments for annotation editor export const FF_DEV_2887 = "fflag-feat-dev-2887-comments-ui-editor-short"; diff --git a/web/libs/editor/src/components/InteractiveOverlays/BoundingBox.js b/web/libs/editor/src/components/InteractiveOverlays/BoundingBox.js index b8f5e0023a53..fc1bbb67a3fb 100644 --- a/web/libs/editor/src/components/InteractiveOverlays/BoundingBox.js +++ b/web/libs/editor/src/components/InteractiveOverlays/BoundingBox.js @@ -1,4 +1,3 @@ -import { FF_DEV_2715, isFF } from "../../utils/feature-flags"; import { wrapArray } from "../../utils/utilities"; import { Geometry } from "./Geometry"; @@ -82,19 +81,10 @@ const _detect = (region) => { return Geometry.getDOMBBox(region.from_name.elementRef?.current); } - let type = region.type; - if (type === "audioregion") { - if (isFF(FF_DEV_2715)) { - type = "audioregion::ultra"; - } else { - type = "audioregion::old"; - } - } - switch (type) { + switch (region.type) { case "textrange": case "richtextregion": case "textarearegion": - case "audioregion::old": case "paragraphs": case "timeseriesregion": { const regionBbox = Geometry.getDOMBBox(region.getRegionElement()); @@ -114,7 +104,7 @@ const _detect = (region) => { return regionBbox; } - case "audioregion::ultra": { + case "audioregion": { const bbox = region.bboxCoordsCanvas; const stageEl = region.parent?.stageRef?.current; const stageBbox = Geometry.getDOMBBox(stageEl, true); diff --git a/web/libs/editor/src/components/InteractiveOverlays/NodesConnector.js b/web/libs/editor/src/components/InteractiveOverlays/NodesConnector.js index a1fe1aeda2ad..c078b75dd415 100644 --- a/web/libs/editor/src/components/InteractiveOverlays/NodesConnector.js +++ b/web/libs/editor/src/components/InteractiveOverlays/NodesConnector.js @@ -1,5 +1,5 @@ import { debounce } from "../../utils/debounce"; -import { FF_DEV_2715, FF_PER_FIELD_COMMENTS, isFF } from "../../utils/feature-flags"; +import { FF_PER_FIELD_COMMENTS, isFF } from "../../utils/feature-flags"; import { wrapArray } from "../../utils/utilities"; import { Geometry } from "./Geometry"; import { RelationShape } from "./RelationShape"; @@ -29,13 +29,7 @@ const obtainWatcher = (node) => { case "paragraphs": return DOMWatcher; case "audioregion": { - if (isFF(FF_DEV_2715)) { - return createPropertyWatcher(["bboxTriggers"]); - } - if (node.getRegionElement) { - return DOMWatcher; - } - return null; + return createPropertyWatcher(["bboxTriggers"]); } case "rectangleregion": return createPropertyWatcher(["x", "y", "width", "height", "hidden", parentImagePropsWatch]); diff --git a/web/libs/editor/src/components/SidePanels/DetailsPanel/RegionEditor.tsx b/web/libs/editor/src/components/SidePanels/DetailsPanel/RegionEditor.tsx index 2d442069b6ed..6c1f1c5b4923 100644 --- a/web/libs/editor/src/components/SidePanels/DetailsPanel/RegionEditor.tsx +++ b/web/libs/editor/src/components/SidePanels/DetailsPanel/RegionEditor.tsx @@ -15,7 +15,6 @@ import { import { IconPropertyAngle } from "@humansignal/icons"; import { Checkbox, Select } from "@humansignal/ui"; import { Block, Elem, useBEM } from "../../../utils/bem"; -import { FF_DEV_2715, isFF } from "../../../utils/feature-flags"; import { TimeDurationControl } from "../../TimeDurationControl/TimeDurationControl"; import { TimelineRegionEditor } from "./TimelineRegionEditor"; import "./RegionEditor.scss"; @@ -53,7 +52,7 @@ const IconMapping = { }; const RegionEditorComponent: FC = ({ region }) => { - const isAudioRegion = isFF(FF_DEV_2715) && region.type === "audioregion"; + const isAudioRegion = region.type === "audioregion"; const isTimelineRegion = region.type === "timelineregion"; const Component = isTimelineRegion ? TimelineRegionEditor : isAudioRegion ? AudioRegionProperties : RegionProperties; diff --git a/web/libs/editor/src/components/Timeline/Controls.tsx b/web/libs/editor/src/components/Timeline/Controls.tsx index 0f19d711f965..47e71e40069e 100644 --- a/web/libs/editor/src/components/Timeline/Controls.tsx +++ b/web/libs/editor/src/components/Timeline/Controls.tsx @@ -31,7 +31,6 @@ import type { TimelineProps, TimelineStepFunction, } from "./Types"; -import { FF_DEV_2715, isFF } from "../../utils/feature-flags"; import { AudioControl } from "./Controls/AudioControl"; import { ConfigControl } from "./Controls/ConfigControl"; import { TimeDurationControl } from "../TimeDurationControl/TimeDurationControl"; @@ -175,7 +174,7 @@ export const Controls: FC = memo( return ( - {isFF(FF_DEV_2715) && mediaType === "audio" ? ( + {mediaType === "audio" ? ( renderControls() ) : ( @@ -311,7 +310,7 @@ export const Controls: FC = memo( - {isFF(FF_DEV_2715) && mediaType === "audio" ? ( + {mediaType === "audio" ? ( <> {customControls?.right} ({ value: +v, label: `Speed ${v}` })); - -/** - * Use formatTimeCallback to style the notch labels as you wish, such - * as with more detail as the number of pixels per second increases. - * - * Here we format as M:SS.frac, with M suppressed for times < 1 minute, - * and frac having 0, 1, or 2 digits as the zoom increases. - * - * Note that if you override the default function, you'll almost - * certainly want to override timeInterval, primaryLabelInterval and/or - * secondaryLabelInterval so they all work together. - * - * @param: seconds - * @param: pxPerSec - */ -function formatTimeCallback(seconds, pxPerSec) { - seconds = Number(seconds); - const minutes = Math.floor(seconds / 60); - - seconds = seconds % 60; - - // fill up seconds with zeroes - let secondsStr = Math.round(seconds).toString(); - - if (pxPerSec >= 25 * 10) { - secondsStr = seconds.toFixed(2); - } else if (pxPerSec >= 25 * 1) { - secondsStr = seconds.toFixed(1); - } - - if (minutes > 0) { - if (seconds < 10) { - secondsStr = `0${secondsStr}`; - } - return `${minutes}:${secondsStr}`; - } - return secondsStr; -} - -/** - * Use timeInterval to set the period between notches, in seconds, - * adding notches as the number of pixels per second increases. - * - * Note that if you override the default function, you'll almost - * certainly want to override formatTimeCallback, primaryLabelInterval - * and/or secondaryLabelInterval so they all work together. - * - * @param: pxPerSec - */ -function timeInterval(pxPerSec) { - let retval = 1; - - if (pxPerSec >= 25 * 100) { - retval = 0.01; - } else if (pxPerSec >= 25 * 40) { - retval = 0.025; - } else if (pxPerSec >= 25 * 10) { - retval = 0.1; - } else if (pxPerSec >= 25 * 4) { - retval = 0.25; - } else if (pxPerSec >= 25) { - retval = 1; - } else if (pxPerSec * 5 >= 25) { - retval = 5; - } else if (pxPerSec * 15 >= 25) { - retval = 15; - } else { - retval = Math.ceil(0.5 / pxPerSec) * 60; - } - return retval; -} - -/** - * Return the cadence of notches that get labels in the primary color. - * EG, return 2 if every 2nd notch should be labeled, - * return 10 if every 10th notch should be labeled, etc. - * - * Note that if you override the default function, you'll almost - * certainly want to override formatTimeCallback, primaryLabelInterval - * and/or secondaryLabelInterval so they all work together. - * - * @param pxPerSec - */ -function primaryLabelInterval(pxPerSec) { - let retval = 1; - - if (pxPerSec >= 25 * 100) { - retval = 10; - } else if (pxPerSec >= 25 * 40) { - retval = 4; - } else if (pxPerSec >= 25 * 10) { - retval = 10; - } else if (pxPerSec >= 25 * 4) { - retval = 4; - } else if (pxPerSec >= 25) { - retval = 1; - } else if (pxPerSec * 5 >= 25) { - retval = 5; - } else if (pxPerSec * 15 >= 25) { - retval = 15; - } else { - retval = Math.ceil(0.5 / pxPerSec) * 60; - } - return retval; -} - -/** - * Return the cadence of notches to get labels in the secondary color. - * EG, return 2 if every 2nd notch should be labeled, - * return 10 if every 10th notch should be labeled, etc. - * - * Secondary labels are drawn after primary labels, so if - * you want to have labels every 10 seconds and another color labels - * every 60 seconds, the 60 second labels should be the secondaries. - * - * Note that if you override the default function, you'll almost - * certainly want to override formatTimeCallback, primaryLabelInterval - * and/or secondaryLabelInterval so they all work together. - * - * @param pxPerSec - */ -function secondaryLabelInterval(pxPerSec) { - // draw one every 10s as an example - return Math.floor(10 / timeInterval(pxPerSec)); -} - -export default class Waveform extends React.Component { - constructor(props) { - super(props); - - this.hotkeys = Hotkey("Audio", "Audio Segmentation"); - - this.state = { - src: this.props.src, - pos: 0, - colors: { - waveColor: "var(--color-neutral-subtle)", - progressColor: "var(--color-positive-surface)", - }, - zoom: 0, - zoomY: MIN_ZOOM_Y, - speed: 1, - volume: props.muted ? 0 : 1, - }; - } - - /** - * Handle to change zoom of wave - */ - onChangeZoom = (value) => { - this.setState({ - ...this.state, - zoom: value, - }); - - this.wavesurfer.zoom(value); - }; - - onChangeZoomY = (value) => { - this.setState( - { - ...this.state, - zoomY: value, - }, - this.updateZoomY, - ); - }; - - updateZoomY = throttle(() => { - this.wavesurfer.params.barHeight = this.state.zoomY; - this.wavesurfer.drawBuffer(); - }, 100); - - onChangeVolume = (value) => { - this.setState({ - ...this.state, - volume: value, - }); - - this.wavesurfer.setVolume(value); - }; - - /** - * Handle to change speed of wave - */ - onChangeSpeed = (value) => { - this.setState({ - ...this.state, - speed: value, - }); - - this.wavesurfer.setPlaybackRate(value); - }; - - onZoomPlus = (ev, step = 10) => { - let val = this.state.zoom; - - val = val + step; - if (val > 700) val = 700; - - this.onChangeZoom(val); - ev && ev.preventDefault(); - return false; - }; - - onZoomMinus = (ev, step = 10) => { - let val = this.state.zoom; - - val = val - step; - if (val < 0) val = 0; - - this.onChangeZoom(val); - ev.preventDefault(); - return false; - }; - - onZoomYPlus = (ev, step = 1) => { - let val = this.state.zoomY; - - val = val + step; - if (val > MAX_ZOOM_Y) val = MAX_ZOOM_Y; - - this.onChangeZoomY(val); - ev.preventDefault(); - return false; - }; - - onZoomYMinus = (ev, step = 1) => { - let val = this.state.zoomY; - - val = val - step; - if (val < MIN_ZOOM_Y) val = MIN_ZOOM_Y; - - this.onChangeZoomY(val); - ev && ev.preventDefault(); - return false; - }; - - onWheel = (e) => { - if (e && !e.shiftKey) { - return; - } - if (e && e.shiftKey) { - /** - * Disable scrolling page - */ - e.preventDefault(); - } - - const step = e.deltaY > 0 ? 5 : -5; - - this.onZoomPlus(e, step); - }; - - onBack = () => { - let time = this.wavesurfer.getCurrentTime(); - - if (!time) return false; - time--; - this.wavesurfer.setCurrentTime(time > 0 ? time : 0); - return false; - }; - - componentDidMount() { - const messages = this.props.messages || defaultMessages; - - /** - * @type {import("wavesurfer.js/types/params").WaveSurferParams} - */ - let wavesurferConfigure = { - container: this.$waveform, - waveColor: this.state.colors.waveColor, - height: this.props.height, - backend: "MediaElement", - progressColor: this.state.colors.progressColor, - - splitChannels: true, - cursorWidth: this.props.cursorWidth, - cursorColor: this.props.cursorColor, - barHeight: 1, - }; - - if (this.props.regions) { - wavesurferConfigure = { - ...wavesurferConfigure, - plugins: [ - RegionsPlugin.create({ - dragSelection: { - slop: 5, // slop - }, - }), - TimelinePlugin.create({ - container: "#timeline", // the element in which to place the timeline, or a CSS selector to find it - formatTimeCallback, // custom time format callback. (Function which receives number of seconds and returns formatted string) - timeInterval, // number of intervals that records consists of. Usually it is equal to the duration in minutes. (Integer or function which receives pxPerSec value and returns value) - primaryLabelInterval, // number of primary time labels. (Integer or function which receives pxPerSec value and reurns value) - secondaryLabelInterval, // number of secondary time labels (Time labels between primary labels, integer or function which receives pxPerSec value and reurns value). - primaryColor: "blue", // the color of the modulo-ten notch lines (e.g. 10sec, 20sec). The default is '#000'. - secondaryColor: "blue", // the color of the non-modulo-ten notch lines. The default is '#c0c0c0'. - primaryFontColor: "#000", // the color of the non-modulo-ten time labels (e.g. 10sec, 20sec). The default is '#000'. - secondaryFontColor: "#000", - }), - CursorPlugin.create({ - wrapper: this.$waveform, - showTime: true, - opacity: 1, - }), - ], - }; - } - - this.wavesurfer = WaveSurfer.create({ - ...wavesurferConfigure, - }); - - if (this.props.defaultVolume) { - this.wavesurfer.setVolume(this.props.defaultVolume); - } - - if (this.props.muted) { - this.wavesurfer.setVolume(0); - } - - if (this.props.defaultSpeed) { - this.wavesurfer.setPlaybackRate(this.props.defaultSpeed); - } - - if (this.props.defaultZoom) { - this.wavesurfer.zoom(this.props.defaultZoom); - } - - this.wavesurfer.on("error", (e) => { - const error = String(e.message || e || ""); - const url = this.props.src; - - // just general error message - let body = messages.ERR_LOADING_AUDIO({ attr: this.props.dataField, error, url }); - - // "Failed to fetch" or HTTP error - if (error?.includes("HTTP") || error?.includes("fetch")) { - this.wavesurfer.hadNetworkError = true; - - body = messages.ERR_LOADING_HTTP({ attr: this.props.dataField, error, url }); - } else if (typeof e === "string" && e.includes("media element")) { - // obviously audio cannot be parsed if it was not loaded successfully - // but WS can generate such error even after network errors, so skip it - if (this.wavesurfer.hadNetworkError) return; - // "Error loading media element" - body = "Error while processing audio. Check media format and availability."; - } - - if (this.props.onError) this.props.onError(body); - }); - - /** - * Load data - */ - this.wavesurfer.load(this.props.src); - - /** - * Speed of waveform - */ - this.wavesurfer.setPlaybackRate(this.state.speed); - - const self = this; - - if (this.props.regions) { - /** - * Mouse enter on region - */ - this.wavesurfer.on("region-mouseenter", (reg) => { - reg._region?.onMouseOver(); - }); - - /** - * Mouse leave on region - */ - this.wavesurfer.on("region-mouseleave", (reg) => { - reg._region?.onMouseLeave(); - }); - - /** - * Add region to wave - */ - this.wavesurfer.on("region-created", (reg) => { - const history = self.props.item.annotation.history; - - // if user draw new region the final state will be in `onUpdateEnd` - // so we should skip history action in `addRegion`; - // during annotation init this step will be rewritten at the end - // during undo/redo this action will be skipped the same way - history.setSkipNextUndoState(); - const region = self.props.addRegion(reg); - - if (!region) return; - - reg._region = region; - reg.color = region.selectedregionbg; - - // If the region channel is not set, set it to the audio region channel - if (reg.channelIdx === -1) reg.channelIdx = region.channel; - - reg.on("click", (ev) => region.onClick(self.wavesurfer, ev)); - reg.on("update-end", () => region.onUpdateEnd(self.wavesurfer)); - - reg.on("dblclick", () => { - window.setTimeout(() => { - reg.play(); - }, 0); - }); - - reg.on("out", () => {}); - }); - } - - /** - * Handler of slider - */ - const slider = document.querySelector("#slider"); - - if (slider) { - slider.oninput = function () { - self.wavesurfer.zoom(Number(this.value)); - }; - } - - this.wavesurfer.on("ready", () => { - self.props.onCreate(this.wavesurfer); - - this.wavesurfer.container.onwheel = throttle(this.onWheel, 100); - }); - - this.wavesurfer.on("waveform-ready", () => { - this.props.onReady?.(this.wavesurfer); - }); - - /** - * Pause trigger of audio - */ - this.wavesurfer.on("pause", self.props.handlePlay); - - /** - * Play trigger of audio - */ - this.wavesurfer.on("play", self.props.handlePlay); - - this.wavesurfer.on("seek", self.props.handleSeek); - - if (this.props.regions) { - this.props.onLoad(this.wavesurfer); - } - - this.hotkeys.addNamed("audio:back", this.onBack, `${Hotkey.DEFAULT_SCOPE},${Hotkey.INPUT_SCOPE}`); - } - - componentWillUnmount() { - this.hotkeys.unbindAll(); - this.wavesurfer.unAll(); - } - - setWaveformRef = (node) => { - this.$waveform = node; - }; - - render() { - return ( -
-
- -
- - {this.props.zoom && ( - - -
-
- - - -
-
- { - this.onChangeZoom(value); - }} - /> -
-
- - - -
-
- - -
-
- - - -
-
- { - this.onChangeZoomY(value); - }} - /> -
-
- - - -
-
- - - {this.props.volume && ( -
-
- { - this.onChangeVolume(value); - }} - /> -
-
- -
-
- )} - - - {this.props.speed && ( -