B4J Question Consuming streaming data from HTTP call

bdunkleysmith

Active Member
Licensed User
Longtime User
I am trying to write a B4J app which consumes a livestream of data from a sports game. On connection initial data is sent and then as on-field actions occur further data is sent. Data is in JSON format. The API requires a long running connection. It is not to be treated as a RESTful call, with multiple calls to download the data not required and indeed forbidden.

The livestream can be accessed via https:// . . . . . /read/ID?ak=API_Key. Indeed I can see the initial data in a browser (Chrome) and subsequent action initiated data appears at the bottom.

I have previously written an app which consumed similar data by establishing a TCP socket connection with the data server on the local network and consuming the TCPSocket.InputStream with AsyncStreamsText. But now I'm struggling to work out how in B4J to create a data stream from a HTTP call to the specified URL.

Any suggestions on what framework I should use would be appreciated.
 

bdunkleysmith

Active Member
Licensed User
Longtime User
Further to my original post, I thought I could perhaps use j.GetInputStream as an alternative to TCPSocket.InputStream as the input stream for my app and so below is the key part of my proof of concept code:

B4X:
Sub Process_Globals

    Private MainForm As Form
    Private btnConnect As Button   
    Private AStreams As AsyncStreamsText
    Public j As HttpJob ' Using jOkHttpUtils2 library

End Sub

Sub AppStart (Form1 As Form, Args() As String)
    MainForm = Form1
    MainForm.RootPane.LoadLayout("Form1")
    MainForm.Show
    
    j.Initialize("json", Me)

End Sub

Sub btnConnect_MouseClicked (EventData As MouseEvent)

    j.Download("https:// . . . . . /read/ID?ak=API_Key")
            
    Log("Response: " & j.Response)
    Log("Error: " & j.ErrorMessage)
    Log("Success: " & j.Success)

    If j.Success then AStreams.Initialize(Me, "AStreams", j.GetInputStream, Null)

End Sub

However the log returns:

Response: anywheresoftware.b4h.okhttp.OkHttpClientWrapper$OkHttpResponse@25f7213d
Error:
Success: false

It seems that while I can connect to the specified URL via a browser (Chrome or Edge), the j.Download("https:// . . . . . /read/ID?ak=API_Key") command fails to connect successfully.

Any suggestions?
 
Upvote 0

PatrikCavina

Active Member
Licensed User
You should wait for HttpJob JobDone event.
I think your code should be:
B4X:
Sub Process_Globals
 
    Private MainForm As Form
    Private btnConnect As Button
 
    Private stream As AsyncStreamsText
    Private currentjob As HttpJob
 
End Sub

Sub AppStart (Form1 As Form, Args() As String)
    MainForm = Form1
    MainForm.RootPane.LoadLayout("Form1")
    MainForm.Show
End Sub

Sub btnConnect_MouseClicked (EventData As MouseEvent)

    Dim j As HttpJob '<=== Use new instance for each request
    j.Initialize("json", Me)
    j.Download("https:// . . . . . /read/ID?ak=API_Key")
    Wait For (j) JobDone (j As HttpJob) '<=== Wait for download complete
 
    Log("Response: " & j.Response)
    Log("Error: " & j.ErrorMessage)
    Log("Success: " & j.Success)

    If j.Success Then
        stream.Initialize(Me, "AStreams", j.GetInputStream, Null)
        currentjob = j
    Else
        j.Release
    End If
 
End Sub
 
Last edited:
Upvote 0

bdunkleysmith

Active Member
Licensed User
Longtime User
Thanks @PatrikCavina for your input, but I don't think it's the answer for a couple of reasons:

  • I don't think the JobDone event is ever triggered because being a stream of data, there is no end and this is indicated when I use a browser to look at the data and in Chrome the little circle keeps going around in the tab and

  • The client is to only make one request. Making further requests is not necessary as the server will stream subsequent data automatically and in fact repeated request will result in access to the server being denied.
But I'll investigate your concept in case I'm wrong in regard to the JobDone event.
 
Upvote 0

bdunkleysmith

Active Member
Licensed User
Longtime User
Thanks @Erel Unfortunately the livestream documentation is not very clear and somewhat ambiguous. Initially I thought I would have to use WebSocket, but I could not make a connection with my test bed app using WebSocket.

On reading in the API documentation "This method instantiates a long running HTTP call, which will remain open as long as data is being continually sent." I thought a simple HTTP call via j.Download("https:// . . . . . /read/ID?ak=API_Key") would be OK, particularly as pasting that into my browser returns the JSON which in part contains {"type":"connection","instance":"b3cf42936506975ed2762c451a296f34","status":"CONNECTED"} confirming the browser is connected and then data follows.

So with my B4J app I'm not even getting to the point of handling the returned data - I'm not even achieving a connection.

I will investigate your link re use of Socket + AsyncStreams and report back.
 
Upvote 0

bdunkleysmith

Active Member
Licensed User
Longtime User
I'm not convinced that I need to create a socket and in any case my attempts to do that based on https://www.b4x.com/android/forum/threads/b4x-class-mjpeg-decoder.73702/#content have not been successful.

Part of my reasoning for that is that in the API documentation it says:

The example below provides some sample code in PHP to process the read feed. It connects to the feed, process the data into messages and coverts the data into a PHP data structure. It then calls a callback function to deal with the data.

B4X:
<?php
$url = "https:// . . . . . /read/ID?ak=API_Key";
$r   = new ReaderExample();
$r->run($url, 'processdata');


// Callback function to process the data
function processdata($data)
{
    // Do whatever you need to do with the data
    var_dump($data);
}


class ReaderExample
{
    protected $msg_buffer = '';
    protected $callback = '';
    
    const MAX_LENGTH = 1000000;
    const EOL_SEPARATOR = "\r\n";
    
    public function run($url, $callbackfunction)
    {
        $this->callback = $callbackfunction;
        $curlURL        = $url;
        $ch             = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_WRITEFUNCTION, array(
            $this,
            'on_Read'
        ));
        curl_exec($ch);
        curl_close($ch);
        return 1;
    }
    
    protected function on_Read($ch, $str)
    {
        $this->msg_buffer .= $str;
        while (($p = strrpos($this->msg_buffer, self::EOL_SEPARATOR)) !== false) {
            $msgs             = explode(self::EOL_SEPARATOR, substr($this->msg_buffer, 0, $p));
            $this->msg_buffer = substr($this->msg_buffer, $p + strlen(self::EOL_SEPARATOR));
            foreach ($msgs as $msg)
                $this->on_Read_Message(rtrim($msg, self::EOL_SEPARATOR) . self::EOL_SEPARATOR);
        }
        
        if (strlen($this->msg_buffer) > self::MAX_LENGTH) {
            $this->on_Read_Message($this->msg_buffer);
            $this->msg_buffer = "";
        }
        return strlen($str);
    }
    
    public function on_Read_Message($msgIn)
    {
        $msg = json_decode($msgIn, true);
        if (!$msg) {
            error_log("INVALID MESSAGE $msgIn");
            return false;
        }
        call_user_func($this->callback, $msg);
        return true;
    }
    
}
?>

I have run that code and I see the data returned. While I have little knowledge of PHP and so don't know exactly how this code works, I can't see any reference to sockets and it seems to simply make a call to https:// . . . . . /read/ID?ak=API_Key

Using this simple web browser example I can see the returned data in the Webview window and further data is appended as the server sends additional data:

B4X:
Sub Process_Globals
    Private MainForm As Form   
    Private btnConnect As Button
    Private wv1 As WebView
    Private startPage As String = "https:// . . . . . /read/ID?ak=API_Key"
    Private we,Temp As JavaObject
End Sub

Sub AppStart (Form1 As Form, Args() As String)
    MainForm = Form1
    MainForm.RootPane.LoadLayout("Form1")
    MainForm.Show
    we.InitializeNewInstance("javafx.scene.web.WebEngine",Null)
    Temp = wv1
    we = Temp.RunMethod("getEngine",Null)
End Sub

Sub btnConnect_MouseClicked (EventData As MouseEvent)   
    we.RunMethod("load",Array As Object(startPage))   
End Sub

Sub wv1_PageFinished (Url As String)
    Log("Page finished: " & Url)
End Sub

As I expected the PageFinished event is not triggered as indicated by no log entry being made.

Hopefully good night's sleep and some more head scratching tomorrow may reveal a solution to my problem . . .
 
Upvote 0

bdunkleysmith

Active Member
Licensed User
Longtime User
Your support is incredible @Erel !

Thank you very much for creating that variant of a class for consuming http streams. Given I'm dealing with JSON data I've been able to strip out some of the complexity required to handle streaming video and so I've created an fls class based on your MJPEG and AsyncStreamsText classes.

Here is the fls.bas class:

B4X:
Sub Class_Globals
    Private mTarget As Object
    Private mEventName As String
    Private hc As OkHttpClient
    Private const PackageName As String = "b4j.example" '<------ change as needed
    Public charset As String = "UTF8"
    Private sb As StringBuilder
End Sub

Public Sub Initialize (TargetModule As Object, EventName As String)
    mTarget = TargetModule
    mEventName = EventName
    hc.Initialize("hc")
    sb.Initialize
End Sub

Public Sub Connect(url As String)
    Dim req As OkHttpRequest
    req.InitializeGet(url)
    hc.Execute(req, 0)
End Sub

Private Sub hc_ResponseError (Response As OkHttpResponse, Reason As String, StatusCode As Int, TaskId As Int)
    Log("Error: " & StatusCode)
End Sub

Private Sub hc_ResponseSuccess (Response As OkHttpResponse, TaskId As Int)
    CallSubDelayed2(mTarget, mEventName & "_Connected", True)
    Dim out As JavaObject
    out.InitializeNewInstance($"${PackageName}.fls$MyOutputStream"$, Array(Me))
    Response.GetAsynchronously("req", out, False, 0)
    Wait For req_StreamFinish (Success As Boolean, TaskId As Int)
    Log("Stream finish")
End Sub

Private Sub Data_Available (Buffer() As Byte)
    Dim newDataStart As Int = sb.Length
    sb.Append(BytesToString(Buffer, 0, Buffer.Length, charset))
    Dim s As String = sb.ToString
    Dim start As Int = 0
    For i = newDataStart To s.Length - 1
        Dim c As Char = s.CharAt(i)
        If i = 0 And c = Chr(10) Then '\n...
            start = 1 'might be a broken end of line character
            Continue
        End If
        If c = Chr(10) Then '\n
            CallSubDelayed2(mTarget, mEventName & "_NewText", s.SubString2(start, i))
            start = i + 1
        Else If c = Chr(13) Then '\r
            CallSubDelayed2(mTarget, mEventName & "_NewText", s.SubString2(start, i))
            If i < s.Length - 1 And s.CharAt(i + 1) = Chr(10) Then '\r\n
                i = i + 1
            End If
            start = i + 1
        End If
    Next
    If start > 0 Then sb.Remove(0, start)
End Sub

#if Java
import java.io.*;
public static class MyOutputStream extends OutputStream {
    B4AClass cls;
    public MyOutputStream (B4AClass cls) {
        this.cls = cls;
    }
    
    public void write(int b) throws IOException {
        cls.getBA().raiseEventFromDifferentThread (null, null, 0, "data_available", true, new Object[] {new byte[] {(byte)b}});
    }
    public void write(byte b[], int off, int len) throws IOException {
        byte[] bb = new byte[len];
        System.arraycopy(b, off, bb, 0, len);
        cls.getBA().raiseEventFromDifferentThread (null, null, 0, "data_available", true, new Object[] {bb});
    }
}
#End If

And for completeness and for the information of others who may have to deal with such a situation, here is a skeleton of my app which uses the fls class to connect to the streaming data source and returns the JSON for parsing:

B4X:
Sub Process_Globals
    Private MainForm As Form
    Private btnConnect As Button
    Private f As fls
End Sub

Sub AppStart (Form1 As Form, Args() As String)
        MainForm = Form1
    MainForm.RootPane.LoadLayout("Form1") 'Load the layout file.
    MainForm.Show   
    f.Initialize(Me, "fls1")
End Sub

Sub btnConnect_MouseClicked (EventData As MouseEvent)
    f.Connect("https:// . . . . . /read/ID?ak=API_Key")
End Sub

Sub fls1_Connected(State As Boolean)
    btnConnect.Text = "Connected"
End Sub

Sub fls1_NewText(Text As String)
    Dim parser As JSONParser
    parser.Initialize(Text)
    Dim root As Map = parser.NextObject

'    Remainder of data processing

End Sub

Thanks for all who have helped me along the way.
 
Upvote 0
Top