media_cast_dlna 0.1.0 copy "media_cast_dlna: ^0.1.0" to clipboard
media_cast_dlna: ^0.1.0 copied to clipboard

A powerful Flutter plugin for discovering and controlling DLNA/UPnP media devices. Cast your media to smart TVs, speakers, and other DLNA-enabled devices with ease! Built with Pigeon for type-safe nat [...]

example/lib/main.dart

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:media_cast_dlna/media_cast_dlna.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'DLNA Media Cast Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const DlnaHomePage(),
    );
  }
}

class DlnaHomePage extends StatefulWidget {
  const DlnaHomePage({super.key});

  @override
  State<DlnaHomePage> createState() => _DlnaHomePageState();
}

class _DlnaHomePageState extends State<DlnaHomePage> {
  String _platformVersion = 'Unknown';
  late final MediaCastDlnaApi _api;
  final TextEditingController _customUrlController = TextEditingController();
  List<DlnaDevice> _discoveredDevices = [];
  DlnaDevice? _selectedDevice;
  String? _selectedRendererUdn;
  bool _isDiscovering = false;
  Timer? _discoveryTimer;
  Timer? _playbackInfoTimer;

  // Playback state
  int _currentPosition = 0;
  int _duration = 0;
  int _currentVolume = 50;
  bool _isMuted = false;
  TransportState _transportState = TransportState.stopped;
  String _currentTrackTitle = '';
  String? _currentThumbnailUrl;
  bool _isSliderBeingDragged = false;

  @override
  void initState() {
    super.initState();
    _api = MediaCastDlnaApi();
    initPlatformState();
    _initializeUpnpService();
  }

  Future<void> _initializeUpnpService() async {
    try {
      await _api.initializeUpnpService();
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Failed to initialize UPnP service: $e')),
        );
      }
    }
  }

  Future<void> initPlatformState() async {
    String platformVersion;
    try {
      platformVersion = await _api.getPlatformVersion();
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }

    if (!mounted) return;

    setState(() {
      _platformVersion = platformVersion;
    });
  }

  Future<void> _startDiscovery() async {
    try {
      setState(() {
        _isDiscovering = true;
      });

      // Start discovery
      final options = DiscoveryOptions(timeout: 10);
      await _api.startDiscovery(options);

      // Set up periodic polling for discovered devices
      _discoveryTimer = Timer.periodic(const Duration(seconds: 2), (
        timer,
      ) async {
        if (_isDiscovering && mounted) {
          await _updateDiscoveredDevices();
        } else {
          timer.cancel();
        }
      });

      // Initial device fetch
      await _updateDiscoveredDevices();
    } catch (e) {
      setState(() {
        _isDiscovering = false;
      });
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Discovery failed: $e')));
    }
  }

  Future<void> _updateDiscoveredDevices() async {
    try {
      final devices = await _api.getDiscoveredDevices();
      setState(() {
        // Update discovered devices
        _discoveredDevices = devices;

        // Check if selected device is still available
        if (_selectedRendererUdn != null) {
          final selectedStillExists = devices.any(
            (d) => d.udn == _selectedRendererUdn,
          );
          if (!selectedStillExists) {
            _selectedDevice = null;
            _selectedRendererUdn = null;
          }
        }
      });
    } catch (e) {
      // Ignore errors during polling
    }
  }

  Future<void> _stopDiscovery() async {
    try {
      _discoveryTimer?.cancel();
      await _api.stopDiscovery();
      setState(() {
        _isDiscovering = false;
      });
    } catch (e) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Stop discovery failed: $e')));
    }
  }

  void _selectRenderer(String rendererUdn) {
    setState(() {
      _selectedRendererUdn = rendererUdn;
      _selectedDevice = _discoveredDevices.firstWhere(
        (device) => device.udn == rendererUdn,
      );
    });

    // Start monitoring playback info for selected renderer
    _startPlaybackInfoMonitoring();
  }

  void _startPlaybackInfoMonitoring() {
    _playbackInfoTimer?.cancel();
    if (_selectedRendererUdn != null) {
      _playbackInfoTimer = Timer.periodic(const Duration(seconds: 1), (
        timer,
      ) async {
        if (_selectedRendererUdn != null && mounted) {
          await _updatePlaybackInfo();
        } else {
          timer.cancel();
        }
      });
    }
  }

  Future<void> _updatePlaybackInfo() async {
    if (_selectedRendererUdn == null) return;

    try {
      // Get transport state
      final transportState = await _api.getTransportState(
        _selectedRendererUdn!,
      );

      // Get current position if playing
      int currentPosition = 0;
      if (transportState == TransportState.playing ||
          transportState == TransportState.paused) {
        currentPosition = await _api.getCurrentPosition(_selectedRendererUdn!);
      }

      // Get playback info (includes duration and track info)
      PlaybackInfo? playbackInfo;
      try {
        playbackInfo = await _api.getPlaybackInfo(_selectedRendererUdn!);
      } catch (e) {
        // Ignore errors when getting playback info
      }

      // Get volume info
      VolumeInfo? volumeInfo;
      try {
        volumeInfo = await _api.getVolumeInfo(_selectedRendererUdn!);
      } catch (e) {
        // Ignore errors when getting volume info
      }

      if (mounted) {
        setState(() {
          _transportState = transportState;
          _currentPosition = currentPosition;
          if (playbackInfo != null) {
            _duration = playbackInfo.duration;
            // Parse track title from metadata if available
            _currentTrackTitle = _parseTrackTitleFromMetadata(
              playbackInfo.currentTrackMetadata,
            );
            // Parse thumbnail URL from metadata if available
            _currentThumbnailUrl = _getThumbnailUrlFromMetadata(
              playbackInfo.currentTrackMetadata,
            );
          }
          if (volumeInfo != null) {
            _currentVolume = volumeInfo.volume;
            _isMuted = volumeInfo.muted;
          }
        });
      }
    } catch (e) {
      // Ignore errors during monitoring
    }
  }

  String _parseTrackTitleFromMetadata(MediaMetadata? metadata) {
    if (metadata == null) return '';
    
    // Extract title based on metadata type
    switch (metadata) {
      case AudioMetadata audioMetadata:
        return audioMetadata.title ?? audioMetadata.album ?? 'Unknown Audio';
      case VideoMetadata videoMetadata:
        return videoMetadata.title ?? 'Unknown Video';
      case ImageMetadata imageMetadata:
        return imageMetadata.title ?? 'Unknown Image';
    }
  }

  String? _getThumbnailUrlFromMetadata(MediaMetadata? metadata) {
    if (metadata == null) return null;
    
    // Extract thumbnail/album art URL based on metadata type
    switch (metadata) {
      case AudioMetadata audioMetadata:
        return audioMetadata.albumArtUri;
      case VideoMetadata videoMetadata:
        return videoMetadata.thumbnailUri;
      case ImageMetadata imageMetadata:
        return imageMetadata.thumbnailUri;
    }
  }

  void _showDeviceDetails(DlnaDevice device) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Device Details'),
          content: SingleChildScrollView(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: [
                _buildDetailRow('Name', device.friendlyName),
                _buildDetailRow('UDN', device.udn),
                _buildDetailRow('Type', device.deviceType),
                _buildDetailRow('Manufacturer', device.manufacturerName),
                _buildDetailRow('Model', device.modelName),
                _buildDetailRow('IP Address', device.ipAddress),
                _buildDetailRow('Port', device.port.toString()),
                _buildDetailRow(
                  'Description',
                  device.modelDescription ?? 'No description',
                ),
                if (device.presentationUrl != null)
                  _buildDetailRow('Presentation URL', device.presentationUrl!),
                if (device.iconUrl != null)
                  _buildDetailRow('Icon URL', device.iconUrl!),
              ],
            ),
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('Close'),
            ),
          ],
        );
      },
    );
  }

  Widget _buildDetailRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 120,
            child: Text(
              '$label:',
              style: const TextStyle(fontWeight: FontWeight.bold),
            ),
          ),
          Expanded(
            child: SelectableText(
              value,
              style: const TextStyle(fontFamily: 'monospace'),
            ),
          ),
        ],
      ),
    );
  }

  // Test media options
  final List<Map<String, dynamic>> _testMediaOptions = [
    {
      'title': 'Big Buck Bunny (Video)',
      'url':
          'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
      'type': 'video/mp4',
      'description': 'Open source 3D computer-animated comedy short film',
      'thumbnailUri':
          'https://storage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg',
      'duration': 596, // 9:56
    },
    {
      'title': 'Sintel Trailer (Video)',
      'url':
          'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4',
      'type': 'video/mp4',
      'description': 'Blender Foundation\'s third open movie',
      'thumbnailUri':
          'https://storage.googleapis.com/gtv-videos-bucket/sample/images/Sintel.jpg',
      'duration': 888, // 14:48
    },
    {
      'title': 'Kalimba (Audio)',
      'url':
          'https://www.learningcontainer.com/wp-content/uploads/2020/02/Kalimba.mp3',
      'type': 'audio/mpeg',
      'description': 'Sample audio file for testing',
      'artist': 'Mr. Scruff',
      'album': 'Sample Music',
      'albumArtUri': 'https://picsum.photos/1920/1080',
      'genre': 'Electronic',
      'duration': 330, // 5:30
    },
    {
      'title': 'Sample Image',
      'url': 'https://picsum.photos/1920/1080',
      'type': 'image/jpeg',
      'description': 'Sample image for testing image display',
      'resolution': '1920x1080',
    },
  ];

  Future<void> _playSelectedMedia(Map<String, dynamic> media) async {
    if (_selectedRendererUdn == null) return;

    final mediaUrl = media['url'] as String;
    final mediaType = media['type'] as String;
    final title = media['title'] as String;
    final description = media['description'] as String;

    // Create appropriate MediaMetadata based on media type
    MediaMetadata metadata;
    if (mediaType.startsWith('video/')) {
      metadata = VideoMetadata(
        title: title,
        description: description,
        upnpClass: 'object.item.videoItem.movie',
        thumbnailUri: media['thumbnailUri'] as String?,
        duration: media['duration'] as int?,
        genre: media['genre'] as String?,
        resolution: media['resolution'] as String?,
        bitrate: media['bitrate'] as int?,
      );
    } else if (mediaType.startsWith('audio/')) {
      metadata = AudioMetadata(
        title: title,
        artist: media['artist'] as String? ?? 'Unknown Artist',
        album: media['album'] as String? ?? 'Unknown Album',
        description: description,
        upnpClass: 'object.item.audioItem.musicTrack',
        albumArtUri: media['albumArtUri'] as String?,
        genre: media['genre'] as String?,
        duration: media['duration'] as int?,
        originalTrackNumber: media['trackNumber'] as int?,
      );
    } else if (mediaType.startsWith('image/')) {
      metadata = ImageMetadata(
        title: title,
        description: description,
        upnpClass: 'object.item.imageItem.photo',
        resolution: media['resolution'] as String?,
        thumbnailUri: media['thumbnailUri'] as String?,
        date: media['date'] as String?,
      );
    } else {
      // Fallback for unknown media types
      metadata = AudioMetadata(
        title: title,
        artist: 'Unknown',
        album: 'Unknown',
        description: description,
        upnpClass: 'object.item',
      );
    }

    try {
      // Set media URI first, then play
      await _api.setMediaUri(_selectedRendererUdn!, mediaUrl, metadata);
      await _api.play(_selectedRendererUdn!);
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Playing: $title')));
    } catch (e) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Play failed: $e')));
    }
  }

  Future<void> _playCustomUrl() async {
    if (_selectedRendererUdn == null ||
        _customUrlController.text.trim().isEmpty) {
      return;
    }

    final url = _customUrlController.text.trim();
    final metadata = AudioMetadata(
      title: 'Custom Media',
      artist: 'Unknown',
      album: 'Unknown',
      description: 'Custom Media',
      upnpClass: 'object.item',
    );

    try {
      // Set media URI first, then play
      await _api.setMediaUri(_selectedRendererUdn!, url, metadata);
      await _api.play(_selectedRendererUdn!);
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(const SnackBar(content: Text('Playing custom media')));
    } catch (e) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Play failed: $e')));
    }
  }

  Future<void> _controlPlayback(String action) async {
    if (_selectedRendererUdn == null) return;

    final actions = {
      'play': () => _api.play(_selectedRendererUdn!),
      'pause': () => _api.pause(_selectedRendererUdn!),
      'stop': () => _api.stop(_selectedRendererUdn!),
      'next': () => _api.next(_selectedRendererUdn!),
      'previous': () => _api.previous(_selectedRendererUdn!),
    };

    try {
      await actions[action]?.call();
    } catch (e) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('$action failed: $e')));
    }
  }

  Future<void> _setVolume(int volume) async {
    if (_selectedRendererUdn == null) return;

    try {
      await _api.setVolume(_selectedRendererUdn!, volume);
    } catch (e) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Set volume failed: $e')));
    }
  }

  @override
  void dispose() {
    _discoveryTimer?.cancel();
    _playbackInfoTimer?.cancel();
    _customUrlController.dispose();
    super.dispose();
  }

  Future<void> _seekTo(int positionSeconds) async {
    if (_selectedRendererUdn == null) return;

    try {
      await _api.seek(_selectedRendererUdn!, positionSeconds);
      setState(() {
        _currentPosition = positionSeconds;
      });
    } catch (e) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Seek failed: $e')));
    }
  }

  String _formatDuration(int seconds) {
    final minutes = seconds ~/ 60;
    final remainingSeconds = seconds % 60;
    return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}';
  }

  IconData _getMediaIcon(String mediaType) {
    if (mediaType.startsWith('video/')) {
      return Icons.videocam;
    } else if (mediaType.startsWith('audio/')) {
      return Icons.music_note;
    } else if (mediaType.startsWith('image/')) {
      return Icons.image;
    } else {
      return Icons.file_present;
    }
  }

  Color _getMediaColor(String mediaType) {
    if (mediaType.startsWith('video/')) {
      return Colors.red;
    } else if (mediaType.startsWith('audio/')) {
      return Colors.blue;
    } else if (mediaType.startsWith('image/')) {
      return Colors.green;
    } else {
      return Colors.grey;
    }
  }

  IconData _getTransportStateIcon() {
    switch (_transportState) {
      case TransportState.playing:
        return Icons.play_arrow;
      case TransportState.paused:
        return Icons.pause;
      case TransportState.stopped:
        return Icons.stop;
      case TransportState.transitioning:
        return Icons.sync;
      case TransportState.noMediaPresent:
        return Icons.music_off;
    }
  }

  Color _getTransportStateColor() {
    switch (_transportState) {
      case TransportState.playing:
        return Colors.green;
      case TransportState.paused:
        return Colors.orange;
      case TransportState.stopped:
        return Colors.red;
      case TransportState.transitioning:
        return Colors.blue;
      case TransportState.noMediaPresent:
        return Colors.grey;
    }
  }

  String _getTransportStateText() {
    switch (_transportState) {
      case TransportState.playing:
        return 'Playing';
      case TransportState.paused:
        return 'Paused';
      case TransportState.stopped:
        return 'Stopped';
      case TransportState.transitioning:
        return 'Loading...';
      case TransportState.noMediaPresent:
        return 'No Media';
    }
  }

  Future<void> _toggleMute() async {
    if (_selectedRendererUdn == null) return;

    try {
      await _api.setMute(_selectedRendererUdn!, !_isMuted);
      setState(() {
        _isMuted = !_isMuted;
      });
    } catch (e) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text('Toggle mute failed: $e')));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('DLNA Media Cast Demo'),
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
      ),
      body: CustomScrollView(
        slivers: [
          SliverToBoxAdapter(
            child: Container(
              padding: const EdgeInsets.all(16),
              color: Colors.grey[100],
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('Platform: $_platformVersion'),
                  const SizedBox(height: 8),
                  Text('Discovered devices: ${_discoveredDevices.length}'),
                  if (_selectedDevice != null) ...[
                    const SizedBox(height: 8),
                    Text(
                      'Selected renderer: ${_selectedDevice?.friendlyName ?? "Unknown"}',
                    ),
                  ],
                ],
              ),
            ),
          ),
          SliverToBoxAdapter(
            child: Container(
              padding: const EdgeInsets.all(16),
              child: Row(
                children: [
                  ElevatedButton(
                    onPressed: _isDiscovering ? null : _startDiscovery,
                    child: Text(
                      _isDiscovering ? 'Discovering...' : 'Start Discovery',
                    ),
                  ),
                  const SizedBox(width: 8),
                  ElevatedButton(
                    onPressed: _isDiscovering ? _stopDiscovery : null,
                    child: const Text('Stop Discovery'),
                  ),
                ],
              ),
            ),
          ),
          SliverToBoxAdapter(
            child: SizedBox(
              height: 200,
              child: _discoveredDevices.isEmpty
                  ? const Center(
                      child: Text(
                        'No devices discovered\nTap "Start Discovery" to find DLNA devices',
                        textAlign: TextAlign.center,
                      ),
                    )
                  : ListView.builder(
                      itemCount: _discoveredDevices.length,
                      itemBuilder: (context, index) {
                        final device = _discoveredDevices[index];
                        final isRenderer = device.deviceType.contains(
                          'MediaRenderer',
                        );
                        final isSelected = device.udn == _selectedRendererUdn;

                        return Card(
                          margin: const EdgeInsets.symmetric(
                            horizontal: 16,
                            vertical: 4,
                          ),
                          child: ListTile(
                            leading: Icon(
                              isRenderer ? Icons.tv : Icons.folder,
                              color: isRenderer ? Colors.blue : Colors.orange,
                            ),
                            title: Text(device.friendlyName),
                            subtitle: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(
                                  '${device.manufacturerName} - ${device.modelName}',
                                ),
                                Text('Type: ${device.deviceType}'),
                                Text('IP: ${device.ipAddress}:${device.port}'),
                              ],
                            ),
                            trailing: isRenderer
                                ? Row(
                                    mainAxisSize: MainAxisSize.min,
                                    children: [
                                      ElevatedButton(
                                        onPressed: isSelected
                                            ? null
                                            : () => _selectRenderer(device.udn),
                                        child: Text(
                                          isSelected ? 'Selected' : 'Select',
                                        ),
                                      ),
                                      const SizedBox(width: 8),
                                      IconButton(
                                        onPressed: () =>
                                            _showDeviceDetails(device),
                                        icon: const Icon(Icons.info),
                                        tooltip: 'Device Details',
                                      ),
                                    ],
                                  )
                                : null,
                            selected: isSelected,
                          ),
                        );
                      },
                    ),
            ),
          ),
          if (_selectedRendererUdn != null)
            SliverToBoxAdapter(
              child: Column(
                children: [
                  const Divider(),
                  // Playback controls
                  Card(
                    margin: const EdgeInsets.all(16),
                    child: Padding(
                      padding: const EdgeInsets.all(20),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          const Text(
                            'Playback Controls',
                            style: TextStyle(
                              fontWeight: FontWeight.bold,
                              fontSize: 18,
                            ),
                          ),
                          const SizedBox(height: 16),

                          // Thumbnail/Album Art and Track info
                          Row(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              // Thumbnail/Album Art
                              Container(
                                width: 80,
                                height: 80,
                                decoration: BoxDecoration(
                                  borderRadius: BorderRadius.circular(8),
                                  color: Colors.grey[300],
                                ),
                                child: _currentThumbnailUrl != null
                                    ? ClipRRect(
                                        borderRadius: BorderRadius.circular(8),
                                        child: Image.network(
                                          _currentThumbnailUrl!,
                                          width: 80,
                                          height: 80,
                                          fit: BoxFit.cover,
                                          errorBuilder: (context, error, stackTrace) {
                                            return Container(
                                              width: 80,
                                              height: 80,
                                              decoration: BoxDecoration(
                                                borderRadius: BorderRadius.circular(8),
                                                color: Colors.grey[300],
                                              ),
                                              child: const Icon(
                                                Icons.music_note,
                                                size: 40,
                                                color: Colors.grey,
                                              ),
                                            );
                                          },
                                          loadingBuilder: (context, child, loadingProgress) {
                                            if (loadingProgress == null) {
                                              return child;
                                            }
                                            return Container(
                                              width: 80,
                                              height: 80,
                                              decoration: BoxDecoration(
                                                borderRadius: BorderRadius.circular(8),
                                                color: Colors.grey[300],
                                              ),
                                              child: const Center(
                                                child: CircularProgressIndicator(strokeWidth: 2),
                                              ),
                                            );
                                          },
                                        ),
                                      )
                                    : const Icon(
                                        Icons.music_note,
                                        size: 40,
                                        color: Colors.grey,
                                      ),
                              ),
                              const SizedBox(width: 16),
                              // Track info
                              Expanded(
                                child: Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    if (_currentTrackTitle.isNotEmpty) ...[
                                      Text(
                                        _currentTrackTitle,
                                        style: const TextStyle(
                                          fontWeight: FontWeight.bold,
                                          fontSize: 16,
                                        ),
                                        overflow: TextOverflow.ellipsis,
                                        maxLines: 2,
                                      ),
                                      const SizedBox(height: 8),
                                    ],
                                    // Transport state indicator
                                    Row(
                                      children: [
                                        Icon(
                                          _getTransportStateIcon(),
                                          color: _getTransportStateColor(),
                                          size: 20,
                                        ),
                                        const SizedBox(width: 8),
                                        Text(
                                          _getTransportStateText(),
                                          style: TextStyle(
                                            color: _getTransportStateColor(),
                                          ),
                                        ),
                                      ],
                                    ),
                                  ],
                                ),
                              ),
                            ],
                          ),
                          const SizedBox(height: 16),

                          // Seek slider
                          Column(
                            children: [
                              Slider(
                                value: _isSliderBeingDragged
                                    ? _currentPosition.toDouble()
                                    : (_duration > 0
                                          ? _currentPosition.toDouble()
                                          : 0),
                                min: 0,
                                max: _duration > 0 ? _duration.toDouble() : 100,
                                onChanged: (value) {
                                  setState(() {
                                    _isSliderBeingDragged = true;
                                    _currentPosition = value.round();
                                  });
                                },
                                onChangeEnd: (value) {
                                  _seekTo(value.round());
                                  setState(() {
                                    _isSliderBeingDragged = false;
                                  });
                                },
                              ),
                              Row(
                                mainAxisAlignment:
                                    MainAxisAlignment.spaceBetween,
                                children: [
                                  Text(_formatDuration(_currentPosition)),
                                  Text(_formatDuration(_duration)),
                                ],
                              ),
                            ],
                          ),
                          const SizedBox(height: 16),

                          Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              IconButton(
                                icon: const Icon(Icons.skip_previous, size: 32),
                                onPressed: () => _controlPlayback('previous'),
                              ),
                              const SizedBox(width: 8),
                              IconButton(
                                icon: const Icon(
                                  Icons.play_arrow,
                                  size: 36,
                                  color: Colors.green,
                                ),
                                onPressed: () => _controlPlayback('play'),
                              ),
                              const SizedBox(width: 8),
                              IconButton(
                                icon: const Icon(
                                  Icons.pause,
                                  size: 36,
                                  color: Colors.orange,
                                ),
                                onPressed: () => _controlPlayback('pause'),
                              ),
                              const SizedBox(width: 8),
                              IconButton(
                                icon: const Icon(
                                  Icons.stop,
                                  size: 32,
                                  color: Colors.red,
                                ),
                                onPressed: () => _controlPlayback('stop'),
                              ),
                              const SizedBox(width: 8),
                              IconButton(
                                icon: const Icon(Icons.skip_next, size: 32),
                                onPressed: () => _controlPlayback('next'),
                              ),
                            ],
                          ),
                          const SizedBox(height: 16),
                          // Volume control
                          Row(
                            children: [
                              IconButton(
                                icon: Icon(
                                  _isMuted ? Icons.volume_off : Icons.volume_up,
                                ),
                                onPressed: () => _toggleMute(),
                              ),
                              Expanded(
                                child: Slider(
                                  value: _currentVolume.toDouble(),
                                  min: 0,
                                  max: 100,
                                  divisions: 20,
                                  onChanged: (value) {
                                    setState(() {
                                      _currentVolume = value.round();
                                    });
                                    _setVolume(value.round());
                                  },
                                ),
                              ),
                              Text('${_currentVolume}%'),
                            ],
                          ),
                        ],
                      ),
                    ),
                  ),
                  // Test Media Section
                  Card(
                    margin: const EdgeInsets.all(16),
                    child: Padding(
                      padding: const EdgeInsets.all(20),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          const Text(
                            'Test Media',
                            style: TextStyle(
                              fontWeight: FontWeight.bold,
                              fontSize: 18,
                            ),
                          ),
                          const SizedBox(height: 16),
                          ...(_testMediaOptions.map(
                            (media) => Card(
                              elevation: 2,
                              margin: const EdgeInsets.symmetric(vertical: 4),
                              child: ListTile(
                                leading: Icon(
                                  _getMediaIcon(media['type'] as String),
                                  color: _getMediaColor(
                                    media['type'] as String,
                                  ),
                                ),
                                title: Text(media['title'] as String),
                                subtitle: Text(media['description'] as String),
                                trailing: ElevatedButton(
                                  onPressed: () => _playSelectedMedia(media),
                                  child: const Text('Play'),
                                ),
                              ),
                            ),
                          )),
                          const SizedBox(height: 16),
                          // Custom URL section
                          const Text(
                            'Play Custom URL',
                            style: TextStyle(
                              fontWeight: FontWeight.bold,
                              fontSize: 16,
                            ),
                          ),
                          const SizedBox(height: 8),
                          Row(
                            children: [
                              Expanded(
                                child: TextField(
                                  controller: _customUrlController,
                                  decoration: const InputDecoration(
                                    labelText: 'Enter media URL',
                                    border: OutlineInputBorder(),
                                    hintText: 'https://example.com/media.mp4',
                                  ),
                                ),
                              ),
                              const SizedBox(width: 8),
                              ElevatedButton(
                                onPressed: _playCustomUrl,
                                child: const Text('Play'),
                              ),
                            ],
                          ),
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }
}
10
likes
0
points
129
downloads

Publisher

unverified uploader

Weekly Downloads

A powerful Flutter plugin for discovering and controlling DLNA/UPnP media devices. Cast your media to smart TVs, speakers, and other DLNA-enabled devices with ease! Built with Pigeon for type-safe native interfaces.

Repository (GitHub)
View/report issues

Topics

#dlna #upnp #media-casting #media-renderer #media-server

Documentation

Documentation

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on media_cast_dlna

Packages that implement media_cast_dlna