Skip to content

Commit d95c25f

Browse files
authored
Merge pull request #1623 from StoDevX/ksto-offline
Remove use of react-native-video from KSTO screen
2 parents 9f2d25b + 05d5982 commit d95c25f

File tree

1 file changed

+233
-56
lines changed

1 file changed

+233
-56
lines changed

source/views/streaming/radio.js

Lines changed: 233 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,33 @@
66

77
import React from 'react'
88
import {
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'
1617
import * as c from '../components/colors'
1718
import Icon from 'react-native-vector-icons/Ionicons'
18-
import Video from 'react-native-video'
1919
import {Touchable} from '../components/touchable'
2020
import {TabBarIcon} from '../components/tabbar-icon'
21-
import {promiseTimeout} from '../../lib/promise-timeout'
2221

2322
const kstoStream = 'https://cdn.stobcm.com/radio/ksto1.stream/master.m3u8'
24-
const kstoStatus = 'https://cdn.stobcm.com/radio/ksto1.stream/chunklist.3mu8'
2523
const 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

3431
type Props = {}
3532

3633
type 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+
191364
type ActionButtonProps = {
192365
icon: string,
193366
text: string,
194367
onPress: () => any,
195368
}
369+
196370
const 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

256433
const landscape = StyleSheet.create({

0 commit comments

Comments
 (0)