Compare commits
2 commits
Author | SHA1 | Date | |
---|---|---|---|
31fe6ba022 | |||
4cf9a2a58f |
4 changed files with 82 additions and 18 deletions
32
README.md
32
README.md
|
@ -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
|
||||
|
||||
|  |  |
|
||||
| --------------------------------- |-----------------------------------|
|
||||
| - | - |
|
||||
|
||||
## 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.
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
32
app/model.go
32
app/model.go
|
@ -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)
|
||||
|
|
21
app/view.go
21
app/view.go
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue