app-video-only.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import { h, Component } from '/js/web_modules/preact.js';
  2. import htm from '/js/web_modules/htm.js';
  3. const html = htm.bind(h);
  4. import VideoPoster from './components/video-poster.js';
  5. import { OwncastPlayer } from './components/player.js';
  6. import {
  7. addNewlines,
  8. makeLastOnlineString,
  9. pluralize,
  10. parseSecondsToDurationString,
  11. } from './utils/helpers.js';
  12. import {
  13. URL_CONFIG,
  14. URL_STATUS,
  15. URL_VIEWER_PING,
  16. TIMER_STATUS_UPDATE,
  17. TIMER_STREAM_DURATION_COUNTER,
  18. TEMP_IMAGE,
  19. MESSAGE_OFFLINE,
  20. MESSAGE_ONLINE,
  21. } from './utils/constants.js';
  22. export default class VideoOnly extends Component {
  23. constructor(props, context) {
  24. super(props, context);
  25. this.state = {
  26. configData: {},
  27. playerActive: false, // player object is active
  28. streamOnline: false, // stream is active/online
  29. isPlaying: false,
  30. //status
  31. streamStatusMessage: MESSAGE_OFFLINE,
  32. viewerCount: '',
  33. lastDisconnectTime: null,
  34. };
  35. // timers
  36. this.playerRestartTimer = null;
  37. this.offlineTimer = null;
  38. this.statusTimer = null;
  39. this.streamDurationTimer = null;
  40. this.handleOfflineMode = this.handleOfflineMode.bind(this);
  41. this.handleOnlineMode = this.handleOnlineMode.bind(this);
  42. this.setCurrentStreamDuration = this.setCurrentStreamDuration.bind(this);
  43. // player events
  44. this.handlePlayerReady = this.handlePlayerReady.bind(this);
  45. this.handlePlayerPlaying = this.handlePlayerPlaying.bind(this);
  46. this.handlePlayerEnded = this.handlePlayerEnded.bind(this);
  47. this.handlePlayerError = this.handlePlayerError.bind(this);
  48. // fetch events
  49. this.getConfig = this.getConfig.bind(this);
  50. this.getStreamStatus = this.getStreamStatus.bind(this);
  51. }
  52. componentDidMount() {
  53. this.getConfig();
  54. this.player = new OwncastPlayer();
  55. this.player.setupPlayerCallbacks({
  56. onReady: this.handlePlayerReady,
  57. onPlaying: this.handlePlayerPlaying,
  58. onEnded: this.handlePlayerEnded,
  59. onError: this.handlePlayerError,
  60. });
  61. this.player.init();
  62. }
  63. componentWillUnmount() {
  64. // clear all the timers
  65. clearInterval(this.playerRestartTimer);
  66. clearInterval(this.offlineTimer);
  67. clearInterval(this.statusTimer);
  68. clearInterval(this.streamDurationTimer);
  69. }
  70. // fetch /config data
  71. getConfig() {
  72. fetch(URL_CONFIG)
  73. .then((response) => {
  74. if (!response.ok) {
  75. throw new Error(`Network response was not ok ${response.ok}`);
  76. }
  77. return response.json();
  78. })
  79. .then((json) => {
  80. this.setConfigData(json);
  81. })
  82. .catch((error) => {
  83. this.handleNetworkingError(`Fetch config: ${error}`);
  84. });
  85. }
  86. // fetch stream status
  87. getStreamStatus() {
  88. fetch(URL_STATUS)
  89. .then((response) => {
  90. if (!response.ok) {
  91. throw new Error(`Network response was not ok ${response.ok}`);
  92. }
  93. return response.json();
  94. })
  95. .then((json) => {
  96. this.updateStreamStatus(json);
  97. })
  98. .catch((error) => {
  99. this.handleOfflineMode();
  100. this.handleNetworkingError(`Stream status: ${error}`);
  101. });
  102. // Ping the API to let them know we're an active viewer
  103. fetch(URL_VIEWER_PING).catch((error) => {
  104. this.handleOfflineMode();
  105. this.handleNetworkingError(`Viewer PING error: ${error}`);
  106. });
  107. }
  108. setConfigData(data = {}) {
  109. const { title, summary } = data;
  110. window.document.title = title;
  111. this.setState({
  112. configData: {
  113. ...data,
  114. summary: summary && addNewlines(summary),
  115. },
  116. });
  117. }
  118. // handle UI things from stream status result
  119. updateStreamStatus(status = {}) {
  120. const { streamOnline: curStreamOnline } = this.state;
  121. if (!status) {
  122. return;
  123. }
  124. const { viewerCount, online, lastConnectTime, lastDisconnectTime } = status;
  125. if (status.online && !curStreamOnline) {
  126. // stream has just come online.
  127. this.handleOnlineMode();
  128. } else if (!status.online && curStreamOnline) {
  129. // stream has just flipped offline.
  130. this.handleOfflineMode();
  131. }
  132. this.setState({
  133. viewerCount,
  134. streamOnline: online,
  135. lastDisconnectTime,
  136. lastConnectTime,
  137. });
  138. }
  139. // when videojs player is ready, start polling for stream
  140. handlePlayerReady() {
  141. this.getStreamStatus();
  142. this.statusTimer = setInterval(this.getStreamStatus, TIMER_STATUS_UPDATE);
  143. }
  144. handlePlayerPlaying() {
  145. this.setState({
  146. isPlaying: true,
  147. });
  148. }
  149. // likely called some time after stream status has gone offline.
  150. // basically hide video and show underlying "poster"
  151. handlePlayerEnded() {
  152. this.setState({
  153. playerActive: false,
  154. isPlaying: false,
  155. });
  156. }
  157. handlePlayerError() {
  158. // do something?
  159. this.handleOfflineMode();
  160. this.handlePlayerEnded();
  161. }
  162. // stop status timer and disable chat after some time.
  163. handleOfflineMode() {
  164. clearInterval(this.streamDurationTimer);
  165. this.setState({
  166. streamOnline: false,
  167. streamStatusMessage: MESSAGE_OFFLINE,
  168. });
  169. }
  170. setCurrentStreamDuration() {
  171. let streamDurationString = '';
  172. if (this.state.lastConnectTime) {
  173. const diff = (Date.now() - Date.parse(this.state.lastConnectTime)) / 1000;
  174. streamDurationString = parseSecondsToDurationString(diff);
  175. }
  176. this.setState({
  177. streamStatusMessage: `${MESSAGE_ONLINE} ${streamDurationString}`,
  178. });
  179. }
  180. // play video!
  181. handleOnlineMode() {
  182. this.player.startPlayer();
  183. this.streamDurationTimer = setInterval(
  184. this.setCurrentStreamDuration,
  185. TIMER_STREAM_DURATION_COUNTER
  186. );
  187. this.setState({
  188. playerActive: true,
  189. streamOnline: true,
  190. streamStatusMessage: MESSAGE_ONLINE,
  191. });
  192. }
  193. handleNetworkingError(error) {
  194. console.error(`>>> App Error: ${error}`);
  195. }
  196. render(props, state) {
  197. const {
  198. configData,
  199. viewerCount,
  200. playerActive,
  201. streamOnline,
  202. streamStatusMessage,
  203. lastDisconnectTime,
  204. isPlaying,
  205. } = state;
  206. const { logo = TEMP_IMAGE, customStyles } = configData;
  207. let viewerCountMessage = '';
  208. if (streamOnline && viewerCount > 0) {
  209. viewerCountMessage = html`${viewerCount}
  210. ${pluralize(' viewer', viewerCount)}`;
  211. } else if (lastDisconnectTime) {
  212. viewerCountMessage = makeLastOnlineString(lastDisconnectTime);
  213. }
  214. const mainClass = playerActive ? 'online' : '';
  215. const poster = isPlaying
  216. ? null
  217. : html` <${VideoPoster} offlineImage=${logo} active=${streamOnline} /> `;
  218. return html`
  219. <main class=${mainClass}>
  220. <style>
  221. ${customStyles}
  222. </style>
  223. <div
  224. id="video-container"
  225. class="flex owncast-video-container bg-black w-full bg-center bg-no-repeat flex flex-col items-center justify-start"
  226. >
  227. <video
  228. class="video-js vjs-big-play-centered display-block w-full h-full"
  229. id="video"
  230. preload="auto"
  231. controls
  232. playsinline
  233. ></video>
  234. ${poster}
  235. </div>
  236. <section
  237. id="stream-info"
  238. aria-label="Stream status"
  239. class="flex flex-row justify-between font-mono py-2 px-4 bg-gray-900 text-indigo-200 shadow-md border-b border-gray-100 border-solid"
  240. >
  241. <span class="text-xs">${streamStatusMessage}</span>
  242. <span id="stream-viewer-count" class="text-xs text-right"
  243. >${viewerCountMessage}</span
  244. >
  245. </section>
  246. </main>
  247. `;
  248. }
  249. }