← Back to all posts

LAN File Transfer & Video Streaming in Flutter: Device Discovery, Chunked Streaming, and DLNA Casting

While building a cross-platform file transfer tool, I ran into challenges around automatic device discovery, hotspot network adaptation, and large-file memory management.

This post documents the problems I encountered, my thought process, and the solutions — hoping it helps developers working on similar features.

Motivation

Cross-device file sharing still has too much friction in practice:

I wanted a single tool that works across Android, iOS, Windows, and macOS — no accounts, no internet, no cables, no vendor lock-in. This post documents the technical challenges I hit while building it and how I solved them.

Background

I needed to implement pure LAN-based file transfer with no cloud dependency, supporting Android, iOS, Windows, and macOS.

Two implementation approaches were considered:

ApproachProsCons
Native code per platformBest performanceFour separate codebases to maintain
Flutter + Platform ChannelsSingle UI and business logic layerRequires bridging native network APIs per platform

I chose the second approach. Dart handles unified transfer scheduling and UI rendering, while Platform Channels invoke each platform's socket and network interface capabilities.

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                    Flutter / Dart Layer                     │
│        UI Widgets │ State Mgmt │ Transfer Scheduler         │
├─────────────────────────────────────────────────────────────┤
│                   Platform Channel Bridge                   │
├───────────┬───────────┬───────────┬─────────────────────────┤
│  Android  │    iOS    │   macOS   │         Windows         │
│ NSD/mDNS  │  Bonjour  │  Bonjour  │       mDNS/DNS-SD       │
│  TCP/UDP  │  TCP/UDP  │  TCP/UDP  │         TCP/UDP         │
│ File I/O  │ File I/O  │ File I/O  │        File I/O         │
│  Hotspot  │  ap1/brg  │  bridge0  │    Hotspot Detection    │
└───────────┴───────────┴───────────┴─────────────────────────┘

Core principle: All platform differences are encapsulated in the native layer. The Dart layer only faces a unified abstract interface.

Automatic Device Discovery

Dual-Channel Strategy

Two mainstream approaches exist for LAN device discovery:

Relying solely on either approach carries compatibility risks. The actual implementation is configurable — you pick one strategy based on your scenario:

Many implementations (including mine) expose this as a toggle, switching based on real-world effectiveness in the target environment.

The Critical Detail: UDP Broadcast Address

This is where most tutorials get it wrong.

Many examples broadcast to 255.255.255.255:

// ❌ Limited broadcast address — blocked primarily on iPhone (iOS 15+ Local Network privacy restricts 255.255.255.255, especially in Personal Hotspot mode)
socket.broadcastTo('255.255.255.255', port);

Here's why this fails: when a phone enables its mobile hotspot, it acts as a DHCP server and assigns IP addresses from vendor-specific subnets (could be 192.168.43.x or 192.168.121.x). In these scenarios, the limited broadcast address gets silently dropped by some phones' firewalls.

The correct approach is to compute the subnet-directed broadcast address:

// ✅ Computed from local IP + subnet mask
// Example: local IP = 192.168.121.5, mask = /24
// Broadcast address = 192.168.121.255
InternetAddress subnetBroadcast = InternetAddress('192.168.121.255');
socket.broadcastTo(subnetBroadcast, port);
  1. Get local IP address and subnet mask
  2. Compute network address (IP AND mask)
  3. Set all host bits to 1 → subnet-directed broadcast address

Hotspot Detection (iOS / macOS)

Neither iOS nor macOS exposes a public API to directly query "is personal hotspot active?" However, you can infer it by checking for the presence of specific virtual network interfaces:

PlatformHotspot Interface Name
iOSap1 or bridge100
macOSbridge0 or bridge100

Each platform's native code checks for these interfaces and returns a boolean to the Dart layer:

final isHotspot = await platform.invokeMethod('isHotspotActive');
if (isHotspot) {
  // Use subnet-directed broadcast on hotspot subnet for discovery
}

Streaming Large Files

The Problem

Transferring a single 2GB video file means you can't load the entire file into memory before sending — mobile devices will OOM immediately.

Solution: Chunked Streaming

     Sender                      Receiver    
┌──────────────┐             ┌──────────────┐
│  Open file   │             │ Create dest  │
│  Read chunk  │─TCP Stream─→│ Append write │
│  Send data   │             │  Verify MD5  │
│   Progress   │             │   Progress   │
└──────────────┘             └──────────────┘

Key parameter choices:

ParameterValueRationale
Chunk size64 KB (example)Balances memory footprint vs syscall overhead — actual value depends on platform and file type
TCP Buffer256 KB (example)Tuned for gigabit LAN — adjust based on measured throughput and device memory
Concurrent connections1–3More connections increase context-switching overhead

Memory Footprint Comparison

ApproachPeak Memory (2GB file)
Load entire file into memory> 2 GB (OOM)
Chunked Streaming< 80 MB

Measured Transfer Speeds

Same Wi-Fi 6 router environment:

ScenarioAverage Speed
Android → Windows> 80 MB/s
iPhone → Mac> 80 MB/s
Android → Android (hotspot direct)~35 MB/s

Web Thumbnail Optimization

When browsing photos on your phone from a desktop browser, requesting full-resolution images (12MP ≈ 8MB each) means a 50-photo list costs ~400MB of traffic. Terrible experience.

Solution: Server-Side Base64 Thumbnails Embedded in HTML

<!-- Server generates 200px thumbnail, embeds as Data URL directly -->
<img src="data:image/png;base64,iVBORw0KGgo..." style="width:100px;">

The browser needs only one HTML request to display all thumbnails — no N additional image requests. Total list page traffic drops from 400MB to ~2MB.

Lossless Video Trimming

Users often want to clip a segment from a video without re-encoding quality loss.

MP4/MOV containers store video streams as sequences of keyframes (I-frames) and dependent frames (P/B-frames):

Original: [I1] [P2] [P3] [I4] [P5] [P6] [I7]

Cut point lands on I4:
Output: [I4] [P5] [P6] [I7]  ← lossless

Cut point lands on P5:
Option A: Search backward to nearest keyframe I4
Option B: Locally re-encode only the I4~P5 segment

As long as the cut start point coincides with a keyframe, you can split the container directly without touching any frame data — achieving true zero-quality-loss trimming.

Video Streaming & TV Casting

Beyond file transfer, ShareEasy also supports two media-centric features:

Video Stream Playback

Instead of waiting for a large video to finish transferring before watching, you can stream it directly from another device on the same LAN:

Video Casting to TV via DLNA

ShareEasy can cast video content to DLNA-compatible smart TVs on the same network:

These features are currently available on Android. We follow a phased rollout strategy — new capabilities launch on Android first, then expand to other platforms based on user feedback and demand.

Summary

ProblemSolution
Device discoveryConfigurable: mDNS (power-efficient) or UDP broadcast (multi-subnet penetration)
Broadcast addressSubnet-directed broadcast, not 255.255.255.255
Hotspot detectionCheck platform-specific interface names
Large file transferChunked streaming (configurable chunk size)
Thumbnail loadingServer-generated Base64 embedded in HTML
Video trimmingKeyframe-aligned container splitting
Video stream playbackHTTP Range request + progressive streaming
TV castingDLNA / SSDP discovery + UPnP AVTransport control

I've validated all of these approaches in ShareEasy, available on Google Play if you'd like to try it out.

If you're building something similar and run into issues, feel free to discuss in the comments.

flutter dart lan file-transfer udp-broadcast mdns dlna streaming

Comments