66
77import React from 'react'
88import {
9- StyleSheet ,
10- View ,
11- ScrollView ,
12- Text ,
139 Dimensions ,
1410 Image ,
11+ ScrollView ,
12+ StyleSheet ,
13+ Text ,
14+ View ,
15+ WebView ,
1516} from 'react-native'
1617import * as c from '../components/colors'
1718import Icon from 'react-native-vector-icons/Ionicons'
18- import Video from 'react-native-video'
1919import { Touchable } from '../components/touchable'
2020import { TabBarIcon } from '../components/tabbar-icon'
21- import { promiseTimeout } from '../../lib/promise-timeout'
2221
2322const kstoStream = 'https://cdn.stobcm.com/radio/ksto1.stream/master.m3u8'
24- const kstoStatus = 'https://cdn.stobcm.com/radio/ksto1.stream/chunklist.3mu8'
2523const image = require ( '../../../images/streaming/ksto/ksto-logo.png' )
2624
27- type Viewport = {
28- width : number ,
29- height : number ,
30- }
25+ type Viewport = { width : number , height : number }
3126
32- type PlayState = 'paused' | 'playing' | 'checking'
27+ type HtmlAudioError = { code : number , message : string }
28+
29+ type PlayState = 'paused' | 'playing' | 'checking' | 'loading'
3330
3431type Props = { }
3532
3633type State = {
3734 playState : PlayState ,
38- streamError : ?Object ,
35+ streamError : ?HtmlAudioError ,
3936 uplinkError : ?string ,
4037 viewport : Viewport ,
4138}
@@ -65,62 +62,45 @@ export default class KSTOView extends React.PureComponent<void, Props, State> {
6562 this . setState ( ( ) => ( { viewport : event . window } ) )
6663 }
6764
68- // check the stream uplink status
69- isUplinkUp = async ( ) => {
70- try {
71- await promiseTimeout ( 6000 , fetch ( kstoStatus ) )
72- return true
73- } catch ( err ) {
74- return false
75- }
65+ play = ( ) => {
66+ this . setState ( ( ) => ( { playState : 'checking' } ) )
7667 }
7768
78- onPlay = async ( ) => {
79- this . setState ( ( ) => ( { playState : 'checking' } ) )
69+ pause = ( ) => {
70+ this . setState ( ( ) => ( { playState : 'paused' } ) )
71+ }
8072
81- const uplinkStatus = await this . isUplinkUp ( )
73+ handleStreamPlay = ( ) => {
74+ this . setState ( ( ) => ( { playState : 'playing' } ) )
75+ }
8276
83- if ( uplinkStatus ) {
84- this . setState ( ( ) => ( { playState : 'playing' } ) )
85- } else {
86- this . setState ( ( ) => ( {
87- playState : 'paused' ,
88- uplinkError : 'The KSTO stream is down. Sorry!' ,
89- } ) )
90- }
77+ handleStreamPause = ( ) => {
78+ this . setState ( ( ) => ( { playState : 'paused' } ) )
9179 }
9280
93- onPause = ( ) => {
94- this . setState ( ( ) => ( {
95- playState : 'paused' ,
96- uplinkError : null ,
97- } ) )
81+ handleStreamEnd = ( ) => {
82+ this . setState ( ( ) => ( { playState : 'paused' } ) )
9883 }
9984
100- // error from react-native-video
101- onError = ( e : any ) => {
85+ handleStreamError = ( e : { code : number , message : string } ) => {
10286 this . setState ( ( ) => ( { streamError : e , playState : 'paused' } ) )
10387 }
10488
10589 renderButton = ( state : PlayState ) => {
10690 switch ( state ) {
10791 case 'paused' :
10892 return (
109- < ActionButton icon = "ios-play" text = "Listen" onPress = { this . onPlay } />
93+ < ActionButton icon = "ios-play" text = "Listen" onPress = { this . play } />
11094 )
11195
11296 case 'checking' :
11397 return (
114- < ActionButton
115- icon = "ios-more"
116- text = "Starting"
117- onPress = { this . onPause }
118- />
98+ < ActionButton icon = "ios-more" text = "Starting" onPress = { this . pause } />
11999 )
120100
121101 case 'playing' :
122102 return (
123- < ActionButton icon = "ios-pause" text = "Pause" onPress = { this . onPause } />
103+ < ActionButton icon = "ios-pause" text = "Pause" onPress = { this . pause } />
124104 )
125105
126106 default :
@@ -143,7 +123,12 @@ export default class KSTOView extends React.PureComponent<void, Props, State> {
143123
144124 const error = this . state . uplinkError
145125 ? < Text style = { styles . status } > { this . state . uplinkError } </ Text >
146- : null
126+ : this . state . streamError
127+ ? < Text style = { styles . status } >
128+ Error Code { this . state . streamError . code } :{ ' ' }
129+ { this . state . streamError . message }
130+ </ Text >
131+ : null
147132
148133 const button = this . renderButton ( this . state . playState )
149134
@@ -173,26 +158,215 @@ export default class KSTOView extends React.PureComponent<void, Props, State> {
173158
174159 { button }
175160
176- { this . state . playState === 'playing'
177- ? < Video
178- source = { { uri : kstoStream } }
179- playInBackground = { true }
180- playWhenInactive = { true }
181- paused = { this . state . playState !== 'playing' }
182- onError = { this . onError }
183- />
184- : null }
161+ < StreamPlayer
162+ playState = { this . state . playState }
163+ // onWaiting={this.handleStreamWait }
164+ onEnded = { this . handleStreamEnd }
165+ // onStalled={this.handleStreamStall }
166+ onPlay = { this . handleStreamPlay }
167+ onPause = { this . handleStreamPause }
168+ onError = { this . handleStreamError }
169+ />
185170 </ View >
186171 </ ScrollView >
187172 )
188173 }
189174}
190175
176+ type StreamPlayerProps = {
177+ playState : PlayState ,
178+ onWaiting ?: ( ) = > any ,
179+ onEnded ?: ( ) => any ,
180+ onStalled ?: ( ) => any ,
181+ onPlay ?: ( ) => any ,
182+ onPause ?: ( ) => any ,
183+ onError ?: HtmlAudioError => any ,
184+ }
185+
186+ type HtmlAudioState =
187+ | 'waiting'
188+ | 'ended'
189+ | 'stalled'
190+ | 'playing'
191+ | 'play'
192+ | 'pause'
193+ type HtmlAudioEvent =
194+ | { type : HtmlAudioState }
195+ | { type : 'error' , error : HtmlAudioError }
196+
197+ class StreamPlayer extends React . PureComponent < void , StreamPlayerProps , void > {
198+ _webview : WebView
199+
200+ componentWillReceiveProps ( nextProps : StreamPlayerProps ) {
201+ this . dispatchEvent ( nextProps . playState )
202+ }
203+
204+ componentWillUnmount ( ) {
205+ this . pause ( )
206+ }
207+
208+ dispatchEvent = ( nextPlayState : PlayState ) => {
209+ // console.log('<StreamPlayer> state changed to', nextPlayState)
210+
211+ switch ( nextPlayState ) {
212+ case 'paused' :
213+ return this . pause ( )
214+
215+ case 'loading' :
216+ case 'checking' :
217+ case 'playing' :
218+ return this . play ( )
219+
220+ default :
221+ return
222+ }
223+ }
224+
225+ handleMessage = ( event : any ) => {
226+ const data : HtmlAudioEvent = JSON . parse ( event . nativeEvent . data )
227+
228+ // console.log('<audio> dispatched event', data.type)
229+
230+ switch ( data . type ) {
231+ case 'waiting' :
232+ return this . props . onWaiting && this . props . onWaiting ( )
233+
234+ case 'ended' :
235+ return this . props . onEnded && this . props . onEnded ( )
236+
237+ case 'stalled' :
238+ return this . props . onStalled && this . props . onStalled ( )
239+
240+ case 'pause' :
241+ return this . props . onPause && this . props . onPause ( )
242+
243+ case 'playing' :
244+ case 'play' :
245+ return this . props . onPlay && this . props . onPlay ( )
246+
247+ case 'error' :
248+ return this . props . onError && this . props . onError ( data . error )
249+
250+ default :
251+ return
252+ }
253+ }
254+
255+ pause = ( ) => {
256+ // console.log('sent "pause" message to <audio>')
257+ this . _webview . postMessage ( 'pause' )
258+ }
259+
260+ play = ( ) => {
261+ // console.log('sent "play" message to <audio>')
262+ this . _webview . postMessage ( 'play' )
263+ }
264+
265+ setRef = ( ref : WebView ) => ( this . _webview = ref )
266+
267+ html = url => `
268+ <style>body {background-color: white;}</style>
269+
270+ <title>KSTO Stream</title>
271+
272+ <audio id="player" webkit-playsinline>
273+ <source src="${ url } " />
274+ </audio>
275+
276+ <script>
277+ var player = document.getElementById('player')
278+
279+ /////
280+ /////
281+
282+ document.addEventListener('message', function(event) {
283+ switch (event.data) {
284+ case 'play':
285+ player.play()
286+ break
287+
288+ case 'pause':
289+ player.pause()
290+ break
291+ }
292+ })
293+
294+ /////
295+ /////
296+
297+ function message(data) {
298+ window.postMessage(JSON.stringify(data))
299+ }
300+
301+ function send(event) {
302+ message({type: event.type})
303+ }
304+
305+ function error(event) {
306+ message({
307+ type: event.type,
308+ error: {
309+ code: event.target.error.code,
310+ message: event.target.error.message,
311+ },
312+ })
313+ }
314+
315+ /////
316+ /////
317+
318+ // "waiting" is fired when playback has stopped because of a temporary
319+ // lack of data.
320+ player.addEventListener('waiting', send)
321+
322+ // "ended" is fired when playback or streaming has stopped because the
323+ // end of the media was reached or because no further data is
324+ // available.
325+ player.addEventListener('ended', send)
326+
327+ // "stalled" is fired when the user agent is trying to fetch media data,
328+ // but data is unexpectedly not forthcoming.
329+ player.addEventListener('stalled', send)
330+
331+ // "playing" is fired when playback is ready to start after having been
332+ // paused or delayed due to lack of data.
333+ player.addEventListener('playing', send)
334+
335+ // "pause" is fired when playback has been paused.
336+ player.addEventListener('pause', send)
337+
338+ // "play" is fired when playback has begun.
339+ player.addEventListener('play', send)
340+
341+ // "error" is fired when an error occurs.
342+ player.addEventListener('error', error)
343+
344+ /////
345+ /////
346+
347+ player.play()
348+ </script>`
349+
350+ render ( ) {
351+ return (
352+ < WebView
353+ ref = { this . setRef }
354+ mediaPlaybackRequiresUserAction = { false }
355+ allowsInlineMediaPlayback = { true }
356+ source = { { html : this . html ( kstoStream ) } }
357+ onMessage = { this . handleMessage }
358+ style = { styles . webview }
359+ />
360+ )
361+ }
362+ }
363+
191364type ActionButtonProps = {
192365 icon : string ,
193366 text : string ,
194367 onPress : ( ) => any ,
195368}
369+
196370const ActionButton = ( { icon, text, onPress} : ActionButtonProps ) =>
197371 < Touchable style = { buttonStyles . button } hightlight = { false } onPress = { onPress } >
198372 < View style = { buttonStyles . buttonWrapper } >
@@ -251,6 +425,9 @@ const styles = StyleSheet.create({
251425 marginTop : 15 ,
252426 marginBottom : 5 ,
253427 } ,
428+ webview : {
429+ display : 'none' ,
430+ } ,
254431} )
255432
256433const landscape = StyleSheet . create ( {
0 commit comments