videoProgress.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import React from 'react';
  2. import cls from 'classnames';
  3. import '@douyinfe/semi-foundation/videoPlayer/videoPlayer.scss';
  4. import { cssClasses } from '@douyinfe/semi-foundation/videoPlayer/constants';
  5. import VideoProgressFoundation, { Marker, MarkerListItem, VideoProgressAdapter } from '@douyinfe/semi-foundation/videoPlayer/progressFoundation';
  6. import Tooltip from '../tooltip';
  7. import { noop } from 'lodash';
  8. import { formatTime } from './utils';
  9. import BaseComponent from '../_base/baseComponent';
  10. export interface VideoProgressProps {
  11. value: number;
  12. onChange: (value: number) => void;
  13. className?: string;
  14. max: number;
  15. showTooltip?: boolean;
  16. markers?: Marker[];
  17. bufferedValue: number
  18. }
  19. export interface VideoProgressState {
  20. isDragging: boolean;
  21. isHandleHovering: boolean;
  22. movingInfo: { progress: number; offset: number; value: number } | null;
  23. activeIndex: number
  24. }
  25. export default class VideoProgress extends BaseComponent<VideoProgressProps, VideoProgressState> {
  26. static defaultProps = {
  27. value: 0,
  28. onChange: noop,
  29. max: 100,
  30. showTooltip: true,
  31. };
  32. private sliderRef: React.RefObject<HTMLDivElement>;
  33. private handleRef: React.RefObject<HTMLDivElement>;
  34. private markersList: MarkerListItem[];
  35. foundation: VideoProgressFoundation;
  36. constructor(props: VideoProgressProps) {
  37. super(props);
  38. this.state = {
  39. isDragging: false,
  40. isHandleHovering: false,
  41. movingInfo: null,
  42. activeIndex: -1, // Used to determine which slider the current handle is on under the dragging state
  43. };
  44. this.sliderRef = React.createRef();
  45. this.handleRef = React.createRef();
  46. this.markersList = this.initMarkerList();
  47. this.foundation = new VideoProgressFoundation(this.adapter);
  48. }
  49. get adapter(): VideoProgressAdapter<VideoProgressProps, VideoProgressState> {
  50. return {
  51. ...super.adapter,
  52. getSliderRef: () => this.sliderRef.current,
  53. getMarkersList: () => this.markersList,
  54. setIsDragging: (isDragging: boolean) => this.setState({ isDragging }),
  55. setIsHandleHovering: (isHandleHovering: boolean) => this.setState({ isHandleHovering }),
  56. setActiveIndex: (activeIndex: number) => this.setState({ activeIndex }),
  57. setMovingInfo: (movingInfo: { progress: number; offset: number; value: number } | null) => this.setState({ movingInfo }),
  58. };
  59. }
  60. initMarkerList = () => {
  61. const { markers, max } = this.props;
  62. const hasMarkers = markers && markers.length > 0;
  63. const defaultMarker: MarkerListItem = {
  64. start: 0,
  65. end: max,
  66. left: '0',
  67. title: '',
  68. width: '100%',
  69. };
  70. const newMarkers = hasMarkers ? [...markers] : [defaultMarker];
  71. let markersList: MarkerListItem[] = [];
  72. if (hasMarkers) {
  73. newMarkers.forEach((marker: MarkerListItem | Marker, index: number) => {
  74. const end = index === newMarkers.length - 1 ? max : newMarkers[index + 1].start;
  75. if (!(marker.start > max || end > max)) {
  76. const item = {
  77. left: `${(marker.start / max) * 100}%`,
  78. width: `${max ? (end - marker.start) / max * 100 : 100}%`,
  79. end: end,
  80. start: marker.start,
  81. title: marker.title
  82. };
  83. markersList.push(item);
  84. }
  85. });
  86. } else {
  87. markersList.push(defaultMarker);
  88. }
  89. return markersList;
  90. }
  91. handleMouseEnter = (e: any) => {
  92. this.foundation.handleMouseEvent(e, false);
  93. }
  94. handleMouseMove = (e: any) => {
  95. this.foundation.handleMouseEvent(e, true);
  96. }
  97. renderTooltipContent = () => {
  98. const { movingInfo } = this.state;
  99. if (this.markersList.length > 0 && movingInfo) {
  100. const hoverIndex = this.markersList.findIndex((marker: MarkerListItem) => {
  101. return movingInfo?.value > marker.start && movingInfo?.value < marker.end;
  102. });
  103. return (
  104. <>
  105. <div className={cls(`${cssClasses.PREFIX_PROGRESS}-tooltip-content`)}>
  106. {this.markersList[hoverIndex]?.title}
  107. </div>
  108. <div className={cls(`${cssClasses.PREFIX_PROGRESS}-tooltip-content`)}>
  109. {formatTime(movingInfo.progress * this.props.max)}
  110. </div>
  111. </>
  112. );
  113. }
  114. return movingInfo && formatTime(movingInfo.progress * this.props.max);
  115. }
  116. render() {
  117. const { showTooltip, max, value: currentValue } = this.props;
  118. const { movingInfo, isHandleHovering, isDragging, activeIndex } = this.state;
  119. const sliderContent = (
  120. <div
  121. role="slider"
  122. tabIndex={0}
  123. aria-valuenow={currentValue as number}
  124. ref={this.sliderRef}
  125. className={cls(`${cssClasses.PREFIX_PROGRESS}`)}
  126. onMouseDown={this.foundation.handleMouseDown}
  127. onMouseUp={this.foundation.handleMouseUp}
  128. onMouseEnter={this.handleMouseEnter}
  129. onMouseMove={this.handleMouseMove}
  130. >
  131. <div className={cls(`${cssClasses.PREFIX_PROGRESS}-markers`)}>
  132. {
  133. this.markersList.map((marker: MarkerListItem, index: number) => (
  134. <div
  135. key={`${marker.start}-${index}`}
  136. className={cls(`${cssClasses.PREFIX_PROGRESS}-slider`,
  137. { [`${cssClasses.PREFIX_PROGRESS}-slider-active`]: index === activeIndex && isDragging }
  138. )}
  139. style={{ left: marker.left, width: marker.width }}
  140. onMouseEnter={() => this.foundation.handleSliderMouseEnter(index)}
  141. onMouseLeave={() => this.foundation.handleSliderMouseLeave(index)}
  142. >
  143. <div className={cls(`${cssClasses.PREFIX_PROGRESS}-slider-list`)} />
  144. <div
  145. className={cls(`${cssClasses.PREFIX_PROGRESS}-slider-buffered`)}
  146. style={{ width: this.foundation.getLoadedWidth(marker) }}
  147. />
  148. <div
  149. className={cls(`${cssClasses.PREFIX_PROGRESS}-slider-played`)}
  150. style={{ width: this.foundation.getPlayedWidth(marker) }}
  151. />
  152. </div>
  153. ))
  154. }
  155. </div>
  156. <div
  157. ref={this.handleRef}
  158. className={cls(`${cssClasses.PREFIX_PROGRESS}-handle`)}
  159. style={{
  160. left: `calc(${((max ? (currentValue || 1) / max : 0) * 100)}% - 8px)`,
  161. transform: 'translateY(-50%)',
  162. opacity: (isHandleHovering || isDragging) ? 1 : 0,
  163. transition: 'opacity 0.3s',
  164. pointerEvents: 'none',
  165. }}
  166. />
  167. </div>
  168. );
  169. return showTooltip ? (
  170. <Tooltip
  171. position={'top'}
  172. className={cls(`${cssClasses.PREFIX_PROGRESS}-tooltip`)}
  173. content={this.renderTooltipContent()}
  174. style={{ 'left': movingInfo?.offset }}
  175. >
  176. {sliderContent}
  177. </Tooltip>
  178. ) : sliderContent;
  179. }
  180. }