foundation.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  2. import { numbers } from './constants';
  3. import { throttle } from 'lodash';
  4. export interface VideoPlayerAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  5. getVideo: () => HTMLVideoElement | null;
  6. getVideoWrapper: () => HTMLDivElement | null;
  7. notifyPause: () => void;
  8. notifyPlay: () => void;
  9. notifyQualityChange: (quality: string) => void;
  10. notifyRateChange: (rate: number) => void;
  11. notifyRouteChange: (route: string) => void;
  12. notifyVolumeChange: (volume: number) => void;
  13. setBufferedValue: (bufferedValue: number) => void;
  14. setCurrentTime: (currentTime: number) => void;
  15. setIsError: (isError: boolean) => void;
  16. setIsMirror: (isMirror: boolean) => void;
  17. setIsPlaying: (isPlaying: boolean) => void;
  18. setMuted: (muted: boolean) => void;
  19. setNotificationContent: (content: string) => void;
  20. setPlaybackRate: (rate: number) => void;
  21. setQuality: (quality: string) => void;
  22. setRoute: (route: string) => void;
  23. setShowControls: (showControls: boolean) => void;
  24. setShowNotification: (showNotification: boolean) => void;
  25. setTotalTime: (totalTime: number) => void;
  26. setVolume: (volume: number) => void
  27. }
  28. export default class VideoPlayerFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<VideoPlayerAdapter<P, S>, P, S> {
  29. constructor(adapter: VideoPlayerAdapter<P, S>) {
  30. super({ ...adapter });
  31. }
  32. private controlsTimer: NodeJS.Timeout | null;
  33. private scrollPosition: { x: number; y: number } | null = null;
  34. init() {
  35. const { volume, muted } = this.getProps();
  36. const video = this._adapter.getVideo();
  37. if (video) {
  38. this._adapter.setTotalTime(video.duration);
  39. this.handleVolumeChange(muted ? 0 : volume);
  40. }
  41. this.registerEvent();
  42. }
  43. destroy() {
  44. this.unregisterEvent();
  45. this.clearTimer();
  46. }
  47. shouldShowControlItem(name: string) {
  48. const { controlsList } = this.getProps();
  49. if (controlsList.includes(name)) {
  50. return true;
  51. }
  52. return false;
  53. }
  54. clearTimer() {
  55. if (this.controlsTimer) {
  56. clearTimeout(this.controlsTimer);
  57. }
  58. }
  59. handleMouseMove = throttle(() => {
  60. this._adapter.setShowControls(true);
  61. this.clearTimer();
  62. this.controlsTimer = setTimeout(() => {
  63. this._adapter.setShowControls(false);
  64. }, 3000);
  65. }, 200);
  66. handleTimeChange(value: number) {
  67. const video = this._adapter.getVideo();
  68. if (!video) return;
  69. if (!Number.isNaN(value)) {
  70. video.currentTime = value;
  71. this._adapter.setCurrentTime(value);
  72. }
  73. }
  74. handleTimeUpdate() {
  75. const video = this._adapter.getVideo();
  76. if (!video) return;
  77. this._adapter.setCurrentTime(video.currentTime);
  78. }
  79. handleDurationChange() {
  80. const video = this._adapter.getVideo();
  81. if (!video) return;
  82. this._adapter.setTotalTime(video.duration);
  83. }
  84. handleError() {
  85. this._adapter.setIsError(true);
  86. }
  87. handlePlayOrPause() {
  88. const video = this._adapter.getVideo();
  89. if (!video) return;
  90. video.paused ? this.handlePlay() : this.handlePause();
  91. }
  92. handlePlay() {
  93. const video = this._adapter.getVideo();
  94. if (video) {
  95. video.play();
  96. this._adapter.setIsPlaying(true);
  97. this._adapter.notifyPlay();
  98. }
  99. }
  100. handlePause() {
  101. const video = this._adapter.getVideo();
  102. if (video) {
  103. video.pause();
  104. this._adapter.setIsPlaying(false);
  105. this._adapter.notifyPause();
  106. }
  107. }
  108. handleCanPlay = () => {
  109. this._adapter.setShowNotification(false);
  110. }
  111. handleWaiting = (locale: any) => {
  112. this._adapter.setNotificationContent(locale.loading);
  113. this._adapter.setShowNotification(true);
  114. }
  115. handleStalled = (locale: any) => {
  116. this._adapter.setNotificationContent(locale.stall);
  117. this._adapter.setShowNotification(true);
  118. }
  119. handleProgress = () => {
  120. const video = this._adapter.getVideo();
  121. if (video && video.buffered.length > 0) {
  122. const bufferedEnd = video.buffered.end(video.buffered.length - 1);
  123. this._adapter.setBufferedValue(bufferedEnd);
  124. }
  125. }
  126. handleEnded = () => {
  127. this._adapter.setIsPlaying(false);
  128. this._adapter.setShowControls(true);
  129. }
  130. handleVolumeChange(value: number) {
  131. const video = this._adapter.getVideo();
  132. if (!video) return;
  133. const volume = Math.floor(value > 0 ? value : 0);
  134. video.volume = volume / 100;
  135. this._adapter.setVolume(volume);
  136. this._adapter.setMuted(volume === 0 ? true : false);
  137. }
  138. handleVolumeSilent = () => {
  139. const video = this._adapter.getVideo();
  140. const { volume, muted } = this.getStates();
  141. if (!video) return;
  142. if (muted) {
  143. video.volume = volume / 100;
  144. this._adapter.setVolume(volume);
  145. this._adapter.setMuted(false);
  146. } else {
  147. video.volume = 0;
  148. this._adapter.setMuted(true);
  149. }
  150. }
  151. checkFullScreen() {
  152. const videoWrapper = this._adapter.getVideoWrapper();
  153. if (!videoWrapper) return false;
  154. return !!(
  155. document.fullscreenElement === videoWrapper ||
  156. // @ts-ignore
  157. document?.webkitFullscreenElement === videoWrapper ||
  158. // @ts-ignore
  159. document?.mozFullScreenElement === videoWrapper ||
  160. // @ts-ignore
  161. document?.msFullscreenElement === videoWrapper ||
  162. // @ts-ignore
  163. videoWrapper?.webkitDisplayingFullscreen // iOS Safari 特殊处理
  164. );
  165. }
  166. handleFullscreen = () => {
  167. const videoWrapper = this._adapter.getVideoWrapper();
  168. const isFullScreen = this.checkFullScreen();
  169. if (videoWrapper) {
  170. if (isFullScreen) {
  171. document.exitFullscreen();
  172. } else {
  173. // record scroll position before entering fullscreen
  174. this.scrollPosition = {
  175. x: window.scrollX,
  176. y: window.scrollY
  177. };
  178. videoWrapper.requestFullscreen();
  179. }
  180. }
  181. }
  182. handleRateChange(rate: { label: string; value: number }, locale: any) {
  183. const video = this._adapter.getVideo();
  184. if (!video) return;
  185. video.playbackRate = rate.value;
  186. this._adapter.setPlaybackRate(rate.value);
  187. this._adapter.notifyRateChange(rate.value);
  188. this.handleTemporaryNotification(locale.rateChange.replace('${rate}', rate.label));
  189. }
  190. handleQualityChange(quality: { label: string; value: string }, locale: any) {
  191. this._adapter.setQuality(quality.value);
  192. this._adapter.notifyQualityChange(quality.value);
  193. this.handleTemporaryNotification(locale.qualityChange.replace('${quality}', quality.label));
  194. this.restorePlayPosition();
  195. }
  196. handleRouteChange(route: { label: string; value: string }, locale: any) {
  197. this._adapter.setRoute(route.value);
  198. this._adapter.notifyRouteChange?.(route.value);
  199. this.handleTemporaryNotification(locale.routeChange.replace('${route}', route.label));
  200. this.restorePlayPosition();
  201. }
  202. handleMirror = (locale: any) => {
  203. const { isMirror } = this.getStates();
  204. this._adapter.setIsMirror(!isMirror);
  205. this.handleTemporaryNotification(!isMirror ? locale.mirror : locale.cancelMirror);
  206. }
  207. handlePictureInPicture = () => {
  208. const video = this._adapter.getVideo();
  209. if (!video) return;
  210. video.requestPictureInPicture();
  211. }
  212. handleLeavePictureInPicture = () => {
  213. const video = this._adapter.getVideo();
  214. if (!video) return;
  215. this._adapter.setIsPlaying(!video.paused);
  216. };
  217. handleTemporaryNotification = (content: string) => {
  218. this._adapter.setNotificationContent(content);
  219. this._adapter.setShowNotification(true);
  220. setTimeout(() => {
  221. this._adapter.setShowNotification(false);
  222. }, 1000);
  223. }
  224. restorePlayPosition() {
  225. const video = this._adapter.getVideo();
  226. if (!video) return;
  227. const wasPlaying = !video.paused;
  228. const currentTime = video.currentTime;
  229. const handleLoaded = () => {
  230. video.currentTime = currentTime;
  231. if (wasPlaying) {
  232. video.play();
  233. }
  234. video.removeEventListener('loadeddata', handleLoaded);
  235. };
  236. video.addEventListener('loadeddata', handleLoaded);
  237. }
  238. handleMouseEnterWrapper = () => {
  239. this._adapter.setShowControls(true);
  240. }
  241. handleMouseLeaveWrapper = () => {
  242. const { isPlaying } = this.getStates();
  243. if (isPlaying) {
  244. this._adapter.setShowControls(false);
  245. }
  246. }
  247. handleFullscreenChange = () => {
  248. const isFullScreen = this.checkFullScreen();
  249. if (isFullScreen) {
  250. document.addEventListener('mousemove', this.handleMouseMove);
  251. } else {
  252. // according to the exit fullScreen has two way, Esc && click the button
  253. // so we need to restore scroll position after exiting fullscreen
  254. if (this.scrollPosition) {
  255. setTimeout(() => {
  256. window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
  257. this.scrollPosition = null;
  258. }, 0);
  259. }
  260. document.removeEventListener('mousemove', this.handleMouseMove);
  261. }
  262. }
  263. registerEvent = () => {
  264. const video = this._adapter.getVideo();
  265. if (!video) return;
  266. document.addEventListener('keydown', (e) => this.handleBodyKeyDown(e));
  267. document.addEventListener('fullscreenchange', this.handleFullscreenChange);
  268. video.addEventListener('leavepictureinpicture', this.handleLeavePictureInPicture);
  269. }
  270. unregisterEvent = () => {
  271. const video = this._adapter.getVideo();
  272. if (!video) return;
  273. document.removeEventListener('keydown', (e) => this.handleBodyKeyDown(e));
  274. document.removeEventListener('fullscreenchange', this.handleFullscreenChange);
  275. video.removeEventListener('leavepictureinpicture', this.handleLeavePictureInPicture);
  276. }
  277. handleBodyKeyDown(e: KeyboardEvent) {
  278. const { currentTime, volume } = this.getStates();
  279. const { seekTime } = this.getProps();
  280. if (e.key === ' ') {
  281. this.handlePlayOrPause();
  282. // } else if (e.key === 'ArrowUp') {
  283. // this.handleVolumeChange(volume + numbers.DEFAULT_VOLUME_STEP);
  284. // } else if (e.key === 'ArrowDown') {
  285. // this.handleVolumeChange(volume - numbers.DEFAULT_VOLUME_STEP);
  286. } else if (e.key === 'ArrowLeft') {
  287. this.handleTimeChange(currentTime - seekTime);
  288. } else if (e.key === 'ArrowRight') {
  289. this.handleTimeChange(currentTime + seekTime);
  290. }
  291. }
  292. }