B4A Library Casty - Cast Videos to Google Cast Devices (Chromecast)

DonManfred

Expert
Licensed User
This is a wrap for this Github project. I started doing the wrap back in 2017 but did not got it working. A few tries later i now got it working ;-)

Casty
Author:
DonManfred
Version: 1.0

  • Class representing local media metadata.
    • Fields:
      • KEY_CONTENT_TYPE As String
      • KEY_IMAGES As String
      • KEY_STUDIO As String
      • KEY_SUBTITLE As String
      • KEY_TITLE As String
      • KEY_URL As String
    • Functions:
      • addImage (url As String)
      • fromBundle (wrapper As android.os.Bundle) As de.donmanfred.MediaItem
      • getImage (index As Int) As String
      • hasImage As Boolean
      • toBundle As android.os.Bundle
    • Properties:
      • ContentType As String
      • Duration As Int
      • Images As java.util.ArrayList [read only]
      • Studio As String
      • SubTitle As String
      • Title As String
      • Url As String
  • CastSession
    Class representing local media metadata.
    • Functions:
      • CastSessionWrapper (session As com.google.android.gms.cast.framework.Session)
      • Initialize (session As com.google.android.gms.cast.framework.Session)
      • IsInitialized As Boolean
    • Properties:
      • Category As String [read only]
      • Connected As Boolean [read only]
      • Connecting As Boolean [read only]
      • Disconnected As Boolean [read only]
      • Disconnecting As Boolean [read only]
      • Resuming As Boolean [read only]
      • SessionId As String [read only]
      • SessionRemainingTimeMs As Long [read only]
      • Suspended As Boolean [read only]
  • Casty
    • Events:
      • onCastSessionUpdated (session As Object, remoteMediaClient As Object)
      • onCastStateChanged (State As Int)
      • onConnected()
      • onDisConnected()
      • onSessionEnded (session As Object, info As Int)
      • onSessionEnding (session As Object)
      • onSessionResumed (session As Object, info As Boolean)
      • onSessionResumeFailed (session As Object, info As Int)
      • onSessionResuming (session As Object, info As String)
      • onSessionStarted (session As Object, info As String)
      • onSessionStartFailed (session As Object, info As Int)
      • onSessionStarting (session As Object)
      • onSessionSuspended (session As Object, info As Int)
    • Functions:
      • addMediaRouteMenuItem (menu As android.view.Menu)
      • addMinicontroller
      • buildMediaInfo (title As String, studio As String, subTitle As String, duration As Int, url As String, mimeType As String, imgUrl As String, bigImageUrl As String) As de.donmanfred.MediaItem
      • configure (receiverId As String)
        Sets the custom receiver ID. Should be used in the {@link Application} class.
        receiverId: the custom receiver ID, e.g. Styled Media Receiver - with custom logo and background
      • configureReceiver (receiverId As String)
      • Initialize (EventName As String)
      • loadMediaAndPlay (mediaData As pl.droidsonroids.casty.MediaData)
      • loadMediaAndPlay3 (mediaInfo As com.google.android.gms.cast.MediaInfo, autoPlay As Boolean, position As Long)
      • loadMediaAndPlayInBackground (mediaData As pl.droidsonroids.casty.MediaData)
      • loadMediaAndPlayInBackground3 (mediaInfo As com.google.android.gms.cast.MediaInfo, autoPlay As Boolean, position As Long)
      • pause
      • play
      • seek (time As Long)
      • setUpMediaRouteButton (menu As android.view.Menu, item As Int)
      • setUpMediaRouteButton2 (MediaButton As android.support.v7.app.MediaRouteButton)
      • togglePlayPause
    • Properties:
      • Buffering [read only]
      • Connected As Boolean [read only]
      • Paused [read only]
      • Player As pl.droidsonroids.casty.CastyPlayer [read only]
      • Playing [read only]
      • UpMediaRouteButton As android.support.v7.app.MediaRouteButton [write only]
  • IntroductoryOverlay
    • Events:
      • onOverlayDismissed()
    • Functions:
      • Initialize (EventName As String, item As android.view.MenuItem)
      • onPause
      • onResume
      • remove
      • setSingleTime
      • show
    • Properties:
      • ButtonText As String [write only]
      • FocusRadius As Float [write only]
      • OverlayColor As Int [write only]
      • TitleText As String [write only]
  • MediaDataBuilder
    • Functions:
      • addPhotoUrl (photoUrl As String) As MediaDataBuilder
      • build As pl.droidsonroids.casty.MediaData
      • Initialize (EventName As String, url As String) As MediaDataBuilder
      • IsInitialized As Boolean
      • setAutoPlay (autoPlay As Boolean) As MediaDataBuilder
      • setContentType (contentType As String) As MediaDataBuilder
      • setMediaType (mediaType As Int) As MediaDataBuilder
      • setPosition (position As Long) As MediaDataBuilder
      • setStreamDuration (streamDuration As Long) As MediaDataBuilder
      • setStreamType (streamType As Int) As MediaDataBuilder
      • setSubtitle (subtitle As String) As MediaDataBuilder
      • setTitle (title As String) As MediaDataBuilder
  • MediaRouteButton
    • Functions:
      • BringToFront
      • DesignerCreateView (base As Panel, lw As Label, props As Map)
      • Initialize (EventName As String, AppID As String)
      • Invalidate
      • Invalidate2 (arg0 As android.graphics.Rect)
      • Invalidate3 (arg0 As Int, arg1 As Int, arg2 As Int, arg3 As Int)
      • IsInitialized As Boolean
      • RemoveView
      • RequestFocus As Boolean
      • SendToBack
      • SetBackgroundImage (arg0 As android.graphics.Bitmap) As BitmapDrawable
      • SetColorAnimated (arg0 As Int, arg1 As Int, arg2 As Int)
      • SetLayout (arg0 As Int, arg1 As Int, arg2 As Int, arg3 As Int)
      • SetLayoutAnimated (arg0 As Int, arg1 As Int, arg2 As Int, arg3 As Int, arg4 As Int)
      • SetVisibleAnimated (arg0 As Int, arg1 As Boolean)
    • Properties:
      • Background As android.graphics.drawable.Drawable
      • Color As Int [write only]
      • DialogFactory As android.support.v7.app.MediaRouteDialogFactory
      • Enabled As Boolean
      • Height As Int
      • Left As Int
      • Padding As Int()
      • Parent As Object [read only]
      • RemoteIndicatorDrawable As android.graphics.drawable.Drawable [write only]
      • RouteSelector As android.support.v7.media.MediaRouteSelector
      • Tag As Object
      • Top As Int
      • Visibility As Int [write only]
      • Visible As Boolean
      • Width As Int

Setup:
- Make your App AppCompat-Compatible. It is mandatory as you need to use the Appcompat Menu creation...
B4X:
Sub Activity_CreateMenu(Menu As ACMenu)
    Menu.Clear
    cast.addMediaRouteMenuItem(Menu)
    Menu.Add(2, 4, "Overflow2", Null)
    Menu.Add(3, 5, "Overflow3", Null)
End Sub
and
B4X:
#If Java
public boolean _onCreateOptionsMenu(android.view.Menu menu) {
    if (processBA.subExists("activity_createmenu")) {
        processBA.raiseEvent2(null, true, "activity_createmenu", false, new de.amberhome.objects.appcompat.ACMenuWrapper(menu));
        return true;
    }
    else
        return false;
}
#End If
- Add the following to your main module
B4X:
#Extends: android.support.v7.app.AppCompatActivity
#AdditionalJar: com.google.android.gms:play-services-cast-framework
#AdditionalJar: casty.aar
Add the following code to your Manifest
B4X:
SetApplicationAttribute(android:theme, "@style/MyAppTheme")

CreateResource(values, theme.xml,
<resources>
    <style name="MyAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">#0098FF</item>
        <item name="colorPrimaryDark">#007CF5</item>
        <item name="colorAccent">#AAAA00</item>
        <item name="windowNoTitle">true</item>
        <item name="windowActionBar">false</item>
    </style>
</resources>
)
AddApplicationText(
<meta-data android:value="" android:name="app_id"></meta-data>
)
AddApplicationText(
<activity
  android:name="pl.droidsonroids.casty.ExpandedControlsActivity"
  android:theme="@style/MyAppTheme" />
<meta-data
  android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
  android:value="de.donmanfred.CastOptionsProvider" />
<receiver android:name="com.google.android.gms.cast.framework.media.MediaIntentReceiver" />

<service android:name="com.google.android.gms.cast.framework.media.MediaNotificationService" />
<service android:name="com.google.android.gms.cast.framework.ReconnectionService" />
)
CreateResourceFromFile(Macro, FirebaseAnalytics.GooglePlayBase)
Note that the line
<meta-data android:value="" android:name="app_id"></meta-data>
can be used to define YOUR App-ID though it is not needed. If the String is an Empty string then the default Receiverapp will do the Job.

Update Jan 2020:
It is mandatory to use your own app_id which must be registered at the Google Cast Developer console.


Before you begin

There is a one-time, non-refundable $5 registration fee charged for a Google Cast Developer account. Google charge this fee to encourage higher quality applications utilizing Google Cast.



You can see the Example on how to use everything.

In short you can say that you can start Playing Videos when you successfully connected to the Cast device and a Session is opened.

B4X:
Sub Casty_onConnected()
    Log($"Casty_onConnected()"$)
    Dim mb As MediaDataBuilder
    mb.Initialize("","http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_30fps_normal.mp4").setAutoPlay(True).setContentType("videos/mp4").setMediaType(1).setStreamType(1).setTitle("DonManfred presents").setSubtitle("B4A Casting Video").addPhotoUrl("https://peach.blender.org/wp-content/uploads/bbb-splash.png?x11217")

    cast.loadMediaAndPlayInBackground(mb.build)
End Sub


 

Attachments

Last edited:

JohnC

Well-Known Member
Licensed User
Nice!
 

AnandGupta

Well-Known Member
Licensed User
Don Sir Great .. Oh I do not have words, how you work hard to create and then simply provide it to us.
Thanks a lot.

Regards,

Anand
 

Sasuke Sama

Active Member
Licensed User
1. You always should create a new thread for your questions.
2. Did you tried it and you run into an Error? Which Error?
i dont have a casting device to test things out but wanted to use it in my app thats why i asked sorry :(
also running your example crashes on launch with this error
B4X:
Logger connected to:  Xiaomi POCOPHONE F1
--------- beginning of main
*** Service (starter) Create ***
** Service (starter) Start **
** Activity (main) Create, isFirst = true **
1
** Activity (main) Resume **
main_session_tick (java line: 652)
java.lang.RuntimeException: Object should first be initialized (CastSession).
    at anywheresoftware.b4a.AbsObjectWrapper.getObject(AbsObjectWrapper.java:50)
    at de.donmanfred.CastSessionWrapper.getSessionId(CastSessionWrapper.java:45)
    at de.donmanfred.cast.main._session_tick(main.java:652)
    at java.lang.reflect.Method.invoke(Native Method)
    at anywheresoftware.b4a.BA.raiseEvent2(BA.java:191)
    at anywheresoftware.b4a.objects.Timer$TickTack.run(Timer.java:105)
    at android.os.Handler.handleCallback(Handler.java:873)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:201)
    at android.app.ActivityThread.main(ActivityThread.java:6806)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873)
*** Service (starter) Create ***
** Service (starter) Start **
** Activity (main) Create, isFirst = true **
1
** Activity (main) Resume **
 

AnandGupta

Well-Known Member
Licensed User
Looks like you need the device, which seems obvious,
java.lang.RuntimeException: Object should first be initialized (CastSession).

Regards,

Anand
 

jota

Active Member
Licensed User
Hello, thanks for the library. When it came to testing it, I found the following error:

B4X:
java.lang.Exception:  java.lang.RuntimeException: Object should first be initialized (CastSession).
Error occurred on line: 153 (Main)
java.lang.RuntimeException: Object should first be initialized (CastSession).
    at anywheresoftware.b4a.AbsObjectWrapper.getObject(AbsObjectWrapper.java:50)
    at de.donmanfred.CastSessionWrapper.getSessionId(CastSessionWrapper.java:45)
    at java.lang.reflect.Method.invoke(Native Method)
    at anywheresoftware.b4a.shell.Shell.runMethod(Shell.java:732)
    at anywheresoftware.b4a.shell.Shell.raiseEventImpl(Shell.java:348)
    at anywheresoftware.b4a.shell.Shell.raiseEvent(Shell.java:255)
    at java.lang.reflect.Method.invoke(Native Method)
    at anywheresoftware.b4a.ShellBA.raiseEvent2(ShellBA.java:144)
    at anywheresoftware.b4a.objects.Timer$TickTack.run(Timer.java:105)
    at android.os.Handler.handleCallback(Handler.java:739)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:158)
    at android.app.ActivityThread.main(ActivityThread.java:7224)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)
java.lang.Exception:  java.lang.RuntimeException: Object should first be initialized (CastSession).
I've tried it with Chromecast and Miracast on Samsung S5 and Motorola E5.
 

Syd Wright

Well-Known Member
Licensed User
Don, you may want to add the word "Chromecast" to the title. I remembered you had made a library for the Chromecast but it took me a while to find it again.
Currently it doesn't show up in the Forum search when entering "Chromecast".
 

Syd Wright

Well-Known Member
Licensed User
It would be very much appreciated if you could make Casty work without the need for AppCompat.
I have never used AppCompat and now get errors related to AppCompat in Casty (Yes I did follow AppCompat setup instructions).
I don't want to mess up my Android system just to cater for AppCompat.
Thanks and Happy Christmas!

PS: B4A warns that SessionTimer should be under Process Globals and warns for two missing bal files. I assume only 1.bal is needed.
 

DonManfred

Expert
Licensed User

Syd Wright

Well-Known Member
Licensed User
the github project is based on it... It is also the way Google documented it. Sorry, AppCompat is mandatory for Casting.
See https://developers.google.com/cast/docs/android_sender/integrate
OK, thanks. I think I have got AppCompat working but Casty crashes on the sessiontimer:
Log($"${ActiveSession.SessionId}: Time left: ${ConvertMillisecondsToString(ActiveSession.SessionRemainingTimeMs)}"$)
with the error: java.lang.RuntimeException: Object should first be initialized (CastSession).
If I stop the timer no other errors appear, but nothing happens. I can see the menu top-right with 2 items.
 

DonManfred

Expert
Licensed User
Are you running the example? Or did you implemented it in your app?

I just tried my exampleproject. Got a few missing dependencies (maybe due to updated artifacts).
B4X:
#AdditionalJar: dagger-android-2.23.2.aar
#AdditionalJar: dagger-2.23.2
#AdditionalJar: javax.inject-1
Added them and then the example works as expected.

B4X:
Sub Activity_CreateMenu(Menu As ACMenu)
    Menu.Clear
    cast.addMediaRouteMenuItem(Menu)
    Menu.Add(2, 4, "Overflow2", Null)
    Menu.Add(3, 5, "Overflow3", Null)
End Sub
It is adding a MediaRouteMenuItem to the Menu (which can be accessed with appcompat).

WhatsApp Image 2019-12-25 at 14.57.01.jpeg

When you click the MediaRouteMenuItem a castingwindow appear where you can start a session.


One of the events raised is

B4X:
Sub Casty_onSessionStarted(session As Object, info As String)
    Log($"Casty_onSessionStarted(${session}, ${info})"$)
    Dim sess As CastSession = session
    Log($"Session: ID ${sess.SessionId} ${sess.Category}"$)
    ActiveSession = sess
End Sub
*** Service (starter) Create ***
** Service (starter) Start **
** Activity (main) Create, isFirst = true **
** Activity (main) Resume **
** Activity (main) Pause, UserClosed = false **
** Activity (main) Resume **
Casty_onCastStateChanged(Not Connected)
** Activity (main) Pause, UserClosed = false **
** Activity (main) Pause, UserClosed = false **
** Activity (main) Resume **
Casty_onCastStateChanged(Connecting to Castdevice)
Casty_onSessionStarting(com.google.android.gms.cast.framework.CastSession@7c40b47)
Session: ID null com.google.android.gms.cast.CATEGORY_CAST/4F8B3483///ALLOW_IPV6
Casty_onCastStateChanged(Connected)
Casty_onSessionStarted(com.google.android.gms.cast.framework.CastSession@7c40b47, a23ab117-9f57-4e27-9b42-a80ccf02d89d)
Session: ID a23ab117-9f57-4e27-9b42-a80ccf02d89d com.google.android.gms.cast.CATEGORY_CAST/4F8B3483///ALLOW_IPV6
Casty_onConnected()
Casty_onCastSessionUpdated()
MediaClient_onProgressUpdated( 0:00:00 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:00 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:01 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:01 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:01 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:01 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:02 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:02 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:02 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:02 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:03 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:03 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:03 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:03 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:04 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:04 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:04 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:04 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:05 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:05 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:05 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:05 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:06 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:06 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:06 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:06 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:07 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:07 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:07 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:08 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:08 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:08 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:08 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:09 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:09 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:09 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:09 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:10 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:10 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:10 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:10 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:11 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:11 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:11 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:11 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:12 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:12 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:12 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:12 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:13 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:13 - 0:10:34)
MediaClient_onProgressUpdated( 0:00:13 - 0:10:34)
Casty_onCastStateChanged(Not Connected)
Casty_onSessionEnding(com.google.android.gms.cast.framework.CastSession@7c40b47)
Session: ID null com.google.android.gms.cast.CATEGORY_CAST/4F8B3483///ALLOW_IPV6
Casty_onSessionEnded()
Session: ID null com.google.android.gms.cast.CATEGORY_CAST/4F8B3483///ALLOW_IPV6
Casty_onDisConnected()
I do not se a timer in my example though...
 

Syd Wright

Well-Known Member
Licensed User
Thank you for your help. Yes I am running your CastyEx example. There is a timer under Globals called
B4X:
  Dim sessiontimer As Timer
and an event (that caused the crash):
B4X:
Sub Session_Tick
    Log($"${ActiveSession.SessionId}: Time left: ${ConvertMillisecondsToString(ActiveSession.SessionRemainingTimeMs)}"$)
End Sub
You may want to add the three missing #AdditionalJar's to this thread. I found Dagger in the Forum but cannot find
javax.inject-1
 

Syd Wright

Well-Known Member
Licensed User
Thank you for adding the three AdditionalJars. I added them to the library and to your CastyEx example, but still nothing happens.
The App shows a blank screen and no errors appear in the log and in DeBug mode no errors are found. None of the Casty events fire.
The two Menu items appear but there is no response when clicking on them (no idea what they are supposed to do).
I have removed the Timer because it still generates the earlier reported error. My Chromecast is switched on and the TV is set to the Chromecast HDMI input.
My device is running Android 6 and B4A version is 8.3.

PS, I did not change anything in CastyEx, except from adding the three Jars, but do notice that targetSdkVersion="21". Please also test with "26".
 
Last edited:

Syd Wright

Well-Known Member
Licensed User
Since Christmas I have been vigorously trying to get Casty to work, but helas to no avail. My suspicion is that I am still missing some components (aar files?) and/or that my Android system or B4A (version 8.3) is not up to date enough. The fact that the timer (that I lateron removed on advice of DonManfred) gives a "Casty not initialized" error is probably a clue. Some more help and advice would be very much appreciated!
 
Top