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
@@ -165,15 +150,15 @@ export default class KSTOView extends React.PureComponent<void, Props, State> {
165150 { error }
166151 { button }
167152
168- { this . state . playState === 'playing'
169- ? < Video
170- source = { { uri : kstoStream } }
171- playInBackground = { true }
172- playWhenInactive = { true }
173- paused = { this . state . playState !== 'playing' }
174- onError = { this . onError }
175- />
176- : null }
153+ < StreamPlayer
154+ playState = { this . state . playState }
155+ // onWaiting={this.handleStreamWait }
156+ onEnded = { this . handleStreamEnd }
157+ // onStalled={this.handleStreamStall }
158+ onPlay = { this . handleStreamPlay }
159+ onPause = { this . handleStreamPause }
160+ onError = { this . handleStreamError }
161+ />
177162 </ View >
178163 </ ScrollView >
179164 )
@@ -190,11 +175,200 @@ const Title = () =>
190175 </ Text >
191176 </ View >
192177
178+ type StreamPlayerProps = {
179+ playState : PlayState ,
180+ onWaiting ?: ( ) => any ,
181+ onEnded ?: ( ) => any ,
182+ onStalled ?: ( ) => any ,
183+ onPlay ?: ( ) => any ,
184+ onPause ?: ( ) => any ,
185+ onError ?: HtmlAudioError => any ,
186+ }
187+
188+ type HtmlAudioState =
189+ | 'waiting'
190+ | 'ended'
191+ | 'stalled'
192+ | 'playing'
193+ | 'play'
194+ | 'pause'
195+ type HtmlAudioEvent =
196+ | { type : HtmlAudioState }
197+ | { type : 'error' , error : HtmlAudioError }
198+
199+ class StreamPlayer extends React . PureComponent < void , StreamPlayerProps , void > {
200+ _webview : WebView
201+
202+ componentWillReceiveProps ( nextProps : StreamPlayerProps ) {
203+ this . dispatchEvent ( nextProps . playState )
204+ }
205+
206+ componentWillUnmount ( ) {
207+ this . pause ( )
208+ }
209+
210+ dispatchEvent = ( nextPlayState : PlayState ) => {
211+ // console.log('<StreamPlayer> state changed to', nextPlayState)
212+
213+ switch ( nextPlayState ) {
214+ case 'paused' :
215+ return this . pause ( )
216+
217+ case 'loading' :
218+ case 'checking' :
219+ case 'playing' :
220+ return this . play ( )
221+
222+ default :
223+ return
224+ }
225+ }
226+
227+ handleMessage = ( event : any ) => {
228+ const data : HtmlAudioEvent = JSON . parse ( event . nativeEvent . data )
229+
230+ // console.log('<audio> dispatched event', data.type)
231+
232+ switch ( data . type ) {
233+ case 'waiting' :
234+ return this . props . onWaiting && this . props . onWaiting ( )
235+
236+ case 'ended' :
237+ return this . props . onEnded && this . props . onEnded ( )
238+
239+ case 'stalled' :
240+ return this . props . onStalled && this . props . onStalled ( )
241+
242+ case 'pause' :
243+ return this . props . onPause && this . props . onPause ( )
244+
245+ case 'playing' :
246+ case 'play' :
247+ return this . props . onPlay && this . props . onPlay ( )
248+
249+ case 'error' :
250+ return this . props . onError && this . props . onError ( data . error )
251+
252+ default :
253+ return
254+ }
255+ }
256+
257+ pause = ( ) => {
258+ // console.log('sent "pause" message to <audio>')
259+ this . _webview . postMessage ( 'pause' )
260+ }
261+
262+ play = ( ) => {
263+ // console.log('sent "play" message to <audio>')
264+ this . _webview . postMessage ( 'play' )
265+ }
266+
267+ setRef = ( ref : WebView ) => ( this . _webview = ref )
268+
269+ html = url => `
270+ <style>body {background-color: white;}</style>
271+
272+ <title>KSTO Stream</title>
273+
274+ <audio id="player" webkit-playsinline>
275+ <source src="${ url } " />
276+ </audio>
277+
278+ <script>
279+ var player = document.getElementById('player')
280+
281+ /////
282+ /////
283+
284+ document.addEventListener('message', function(event) {
285+ switch (event.data) {
286+ case 'play':
287+ player.play()
288+ break
289+
290+ case 'pause':
291+ player.pause()
292+ break
293+ }
294+ })
295+
296+ /////
297+ /////
298+
299+ function message(data) {
300+ window.postMessage(JSON.stringify(data))
301+ }
302+
303+ function send(event) {
304+ message({type: event.type})
305+ }
306+
307+ function error(event) {
308+ message({
309+ type: event.type,
310+ error: {
311+ code: event.target.error.code,
312+ message: event.target.error.message,
313+ },
314+ })
315+ }
316+
317+ /////
318+ /////
319+
320+ // "waiting" is fired when playback has stopped because of a temporary
321+ // lack of data.
322+ player.addEventListener('waiting', send)
323+
324+ // "ended" is fired when playback or streaming has stopped because the
325+ // end of the media was reached or because no further data is
326+ // available.
327+ player.addEventListener('ended', send)
328+
329+ // "stalled" is fired when the user agent is trying to fetch media data,
330+ // but data is unexpectedly not forthcoming.
331+ player.addEventListener('stalled', send)
332+
333+ // "playing" is fired when playback is ready to start after having been
334+ // paused or delayed due to lack of data.
335+ player.addEventListener('playing', send)
336+
337+ // "pause" is fired when playback has been paused.
338+ player.addEventListener('pause', send)
339+
340+ // "play" is fired when playback has begun.
341+ player.addEventListener('play', send)
342+
343+ // "error" is fired when an error occurs.
344+ player.addEventListener('error', error)
345+
346+ /////
347+ /////
348+
349+ player.play()
350+ </script>`
351+
352+ render ( ) {
353+ return (
354+ < WebView
355+ ref = { this . setRef }
356+ mediaPlaybackRequiresUserAction = { false }
357+ allowsInlineMediaPlayback = { true }
358+ source = { { html : this . html ( kstoStream ) } }
359+ onMessage = { this . handleMessage }
360+ style = { styles . webview }
361+ />
362+ )
363+ }
364+ }
365+
193366type ActionButtonProps = {
194367 icon : string ,
195368 text : string ,
196369 onPress : ( ) = > any ,
197370}
371+
198372const ActionButton = ( { icon, text, onPress} : ActionButtonProps ) =>
199373 < Touchable style = { buttonStyles . button } hightlight = { false } onPress = { onPress } >
200374 < View style = { buttonStyles . buttonWrapper } >
@@ -251,6 +425,9 @@ const styles = StyleSheet.create({
251425 fontSize : 24 ,
252426 color : c . grapefruit ,
253427 } ,
428+ webview : {
429+ display : 'none' ,
430+ } ,
254431} )
255432
256433const landscape = StyleSheet . create ( {
0 commit comments