Compare commits

..

2 commits
v0.1.0 ... main

Author SHA1 Message Date
31fe6ba022 Clarify project scope 2025-06-16 10:38:51 +02:00
4cf9a2a58f
Improve casting resume behavior 2025-06-07 22:43:22 +02:00
4 changed files with 82 additions and 18 deletions

View file

@ -1,18 +1,36 @@
# FCaster
FCaster is a native cross-platform application to cast media to a [FCast](https://fcast.org) Receiver, built with [Fyne](https://fyne.io).
The protocol logic is all packaged into the [FCast Library](https://pkg.go.dev/git.massive.box/massivebox/fcaster/fcast), which you can use in your own projects to build a FCast Sender!
FCaster is a Go [library](https://pkg.go.dev/git.massive.box/massivebox/fcaster/fcast) implementing a [FCast](https://fcast.org) Sender, which allows to send media to a Receiver. It also has a simple cross-platform application, built with [Fyne](https://fyne.io), to showcase its features.
Currently, the app only allows you to stream from URLs, but I will add a way to stream local files and Jellyfin media directly. (You can already stream Jellyfin media by copying the stream URL from Jellyfin and pasting it in the app - [detailed instructions](https://s.massive.box/fcaster-jellyfin)).
**Notice**: FCaster is NOT associated to, or endorsed by, FUTO. It's an unofficial implementation of the FCast Protocol.
If you're on Android, I recommend you [disable battery optimizations](https://support.google.com/pixelphone/thread/299966895/turn-off-battery-optimization-for-an-app?hl=en) for FCaster, otherwise the app will disconnect when your device locks.
## Library
## Screenshots
The [FCast library](https://pkg.go.dev/git.massive.box/massivebox/fcaster/fcast) allows you to stream media to a FCast Receiver from your own software!
| Feature | Supported |
|-------------------|-----------|
| TCP | ✅ |
| WebSockets | ❌ |
| mDNS Discovery | ✅ |
| Outgoing Commands | ✅ |
| Incoming Commands | ✅ |
| QR Code | ❌ |
Since the library is in active development, its stability is not guaranteed.
## App
Due to the limitations of Fyne on Android (not implementing [foreground services](https://developer.android.com/guide/components/foreground-services)), the FCaster App will not support features such as local media casting in the near future. We are working on an alternative implementation.
Currently, the app only allows you to stream from URLs. (You can already stream Jellyfin media by copying the stream URL from Jellyfin and pasting it in the app - [detailed instructions](https://s.massive.box/fcaster-jellyfin)).
### Screenshots
| ![Main view](assets/mainView.png) | ![Cast view](assets/castView.png) |
| --------------------------------- |-----------------------------------|
| - | - |
## Download
### Download
Head over to the [Releases](https://git.massive.box/massivebox/fcaster/releases) page to find builds for your device. You can use [Obtanium](https://obtainium.imranr.dev/) to get automatic updates on Android.

View file

@ -39,16 +39,10 @@ func (c *Controller) StartCasting(selected string, mediaURL string) {
return
}
err := c.Model.ConnectToDevice(selected)
if err != nil {
c.LogAndShowError(fmt.Errorf("error connecting to device %v", err))
return
}
go func() {
err = c.Model.StartCast(mediaURL)
err := c.Model.Cast(selected, mediaURL)
if err != nil {
c.LogAndShowError(fmt.Errorf("error starting cast %v", err))
c.DiscoverDevices()
c.LogAndShowError(fmt.Errorf("error starting cast: %v", err))
return
}
}()
@ -56,6 +50,10 @@ func (c *Controller) StartCasting(selected string, mediaURL string) {
}
func (c *Controller) ShowReconnecting(reconnecting bool) {
c.View.PopupReconnecting(reconnecting)
}
// Note: these are app codes, they are NOT related to fcast opcodes!
const (
ActionPlay = 0
@ -78,7 +76,6 @@ func (c *Controller) PlayerAction(action int, args ...float32) {
}
func (c *Controller) ExitCasting() {
c.PlayerAction(ActionStop)
c.DiscoverDevices()
}

View file

@ -7,6 +7,8 @@ import (
"io"
"math"
"net/http"
"strings"
"time"
)
type Model struct {
@ -14,9 +16,11 @@ type Model struct {
DiscoveredDevices []fcast.DiscoveredHost
Connection *fcast.Connection
EventManager *fcast.EventManager
DeviceName string
Time float32
Length float32
Volume float32
IsReconnecting bool
}
func (m *Model) DiscoverDevices() ([]string, error) {
@ -47,10 +51,29 @@ func (m *Model) ConnectToDevice(name string) error {
return err
}
m.Connection = connection
m.DeviceName = name
return nil
}
func (m *Model) StartCast(mediaURL string) error {
func (m *Model) Cast(selectedDevice, mediaURL string) error {
err := m.doCast(selectedDevice, mediaURL)
if err != nil {
if errors.Is(err, io.EOF) || strings.Contains(err.Error(), "software caused connection abort") {
m.Controller.ShowReconnecting(true)
time.Sleep(500 * time.Millisecond)
return m.Cast(selectedDevice, mediaURL)
}
}
m.Controller.ShowReconnecting(false)
return err
}
func (m *Model) doCast(selectedDevice, mediaURL string) error {
err := m.ConnectToDevice(selectedDevice)
if err != nil {
return fmt.Errorf("error connecting to device %v", err)
}
m.EventManager = &fcast.EventManager{}
@ -81,6 +104,13 @@ func (m *Model) StartCast(mediaURL string) error {
if err != nil {
return err
}
} else {
m.Volume = 1
}
err = m.Connection.SendMessage(&fcast.PingMessage{})
if err == nil {
m.Controller.ShowReconnecting(false)
}
return m.Connection.ListenForMessages(m.EventManager)

View file

@ -18,10 +18,29 @@ type View struct {
Controller *Controller
Window fyne.Window
CastingScreenElements CastingScreenElements
ReconnectingDialog *dialog.CustomDialog
}
func (v *View) PopupError(err error) {
dialog.ShowError(err, v.Window)
fyne.Do(func() {
dialog.ShowError(err, v.Window)
})
}
func (v *View) PopupReconnecting(show bool) {
if v.ReconnectingDialog != nil && !show {
fyne.DoAndWait(func() {
v.ReconnectingDialog.Dismiss()
v.ReconnectingDialog = nil
})
return
}
if v.ReconnectingDialog == nil && show {
v.ReconnectingDialog = dialog.NewCustomWithoutButtons("Reconnecting to stream...", widget.NewProgressBarInfinite(), v.Window)
fyne.Do(func() {
v.ReconnectingDialog.Show()
})
}
}
func NewView(controller *Controller) *View {