Initial commit

This commit is contained in:
2024-12-15 00:52:28 +01:00
commit 540b504c2e
33 changed files with 2217 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
import 'dart:convert';
import 'dart:developer' show log;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../api/commands.dart';
import '../api/events.dart';
import 'player_state_bloc.dart';
class ConnectionStateBloc extends Bloc<ConnectionEvent, WebSocketChannel?> {
final PlayerStateBloc playerStateBloc;
String? _uri;
WebSocketChannel? _channel;
ConnectionStateBloc(this.playerStateBloc) : super(null) {
on<Connect>((event, emit) {
if (_channel != null && _uri == event.uri) {
log('Already connected to ${event.uri}');
return;
} else if (_channel != null) {
// Clear connection, and reconnect
state?.sink.close();
playerStateBloc.add(const ClearPlayerState());
emit(null);
}
_uri = event.uri;
_channel = WebSocketChannel.connect(
Uri.parse(event.uri),
);
_channel!.stream.listen(
(event) {
final jsonData = jsonDecode(event as String);
if (jsonData is Map) {
switch (jsonData['type']) {
case 'initial_state':
playerStateBloc.add(
InitialPlayerState.fromJson(jsonData['value']),
);
break;
case 'event':
final event = parseEvent(jsonData['value']);
if (event == null) {
log('Unknown event: ${jsonData['value']}');
} else {
log('Handling event: $event');
playerStateBloc.add(event);
}
break;
default:
log('Unknown message type: ${jsonData['type']}');
log('Message: $jsonData');
break;
}
}
},
onError: (error) {
log('Error: $error');
},
onDone: () {
add(Disconnect());
log('Connection closed, reconnecting...');
add(Connect(_uri!));
},
);
emit(_channel);
});
on<Disconnect>((event, emit) {
_uri = null;
state?.sink.close(0, 'Disconnecting');
playerStateBloc.add(const ClearPlayerState());
emit(null);
});
on<Command>((event, emit) {
if (_channel == null) {
log('Cannot send command when not connected');
return;
}
_channel!.sink.add(event.toJsonString());
});
}
}

144
lib/state/player_state.dart Normal file
View File

@@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
@immutable
class PlayerState {
final List<Chapter> chapters;
final List<SubtitleTrack> subtitleTracks;
final Playlist playlist;
final String currentTrack;
final bool isLooping;
final bool isMuted;
final bool isPlaying;
final bool isPausedForCache;
final double? cachedTimestamp;
final Duration duration;
final double volume;
final double? currentPercentPosition;
const PlayerState({
required this.cachedTimestamp,
required this.chapters,
required this.currentPercentPosition,
required this.currentTrack,
required this.duration,
required this.isLooping,
required this.isMuted,
required this.isPlaying,
required this.isPausedForCache,
required this.playlist,
required this.subtitleTracks,
required this.volume,
});
factory PlayerState.fromJson(Map<String, dynamic> json) {
return PlayerState(
cachedTimestamp: json['cached_timestamp'],
chapters: (json['chapters'] as List).map((e) => Chapter.fromJson(e)).toList(),
currentPercentPosition: json['current_percent_pos'],
currentTrack: json['current_track'],
duration: Duration(milliseconds: (json['duration'] * 1000).round()),
isLooping: json['is_looping'],
isMuted: json['is_muted'],
isPlaying: json['is_playing'],
isPausedForCache: json['is_paused_for_cache'],
playlist: (json['playlist'] as List).map((e) => PlaylistItem.fromJson(e)).toList(),
subtitleTracks: (json['tracks'] as List).map((e) => SubtitleTrack.fromJson(e)).toList(),
volume: json['volume'],
);
}
PlayerState copyWith({
List<Chapter>? chapters,
List<SubtitleTrack>? subtitleTracks,
Playlist? playlist,
String? currentTrack,
bool? isLooping,
bool? isMuted,
bool? isPlaying,
bool? isPausedForCache,
double? cachedTimestamp,
double? currentPercentPosition,
Duration? duration,
double? volume,
}) {
return PlayerState(
cachedTimestamp: cachedTimestamp ?? this.cachedTimestamp,
chapters: chapters ?? this.chapters,
currentPercentPosition: currentPercentPosition ?? this.currentPercentPosition,
currentTrack: currentTrack ?? this.currentTrack,
duration: duration ?? this.duration,
isLooping: isLooping ?? this.isLooping,
isMuted: isMuted ?? this.isMuted,
isPlaying: isPlaying ?? this.isPlaying,
isPausedForCache: isPausedForCache ?? this.isPausedForCache,
playlist: playlist ?? this.playlist,
subtitleTracks: subtitleTracks ?? this.subtitleTracks,
volume: volume ?? this.volume,
);
}
}
typedef Playlist = List<PlaylistItem>;
@immutable
class PlaylistItem {
final bool current;
final String filename;
final int id;
final String? title;
const PlaylistItem({
required this.current,
required this.filename,
required this.id,
required this.title,
});
factory PlaylistItem.fromJson(Map<String, dynamic> json) {
return PlaylistItem(
current: json['current'] ?? false,
filename: json['filename'],
id: json['id'],
title: json['title'],
);
}
}
@immutable
class Chapter {
final String title;
final double time;
const Chapter({
required this.title,
required this.time,
});
factory Chapter.fromJson(Map<String, dynamic> json) {
return Chapter(
title: json['title'],
time: json['time'],
);
}
}
@immutable
class SubtitleTrack {
final int id;
final String title;
final String? lang;
const SubtitleTrack({
required this.id,
required this.title,
required this.lang,
});
factory SubtitleTrack.fromJson(Map<String, dynamic> json) {
return SubtitleTrack(
id: json['id'],
title: json['title'],
lang: json['lang'],
);
}
}

View File

@@ -0,0 +1,78 @@
import 'dart:developer' show log;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'player_state.dart';
import '../api/events.dart';
class PlayerStateBloc extends Bloc<Event, PlayerState?> {
PlayerStateBloc() : super(null) {
on<InitialPlayerState>((event, emit) {
emit(event.playerState);
});
on <ClearPlayerState>((event, emit) {
emit(null);
});
on<PropertyChangedEvent>((event, emit) {
// print('Received event: $event');
if (state == null) {
log('Received event before initial state');
return;
}
switch (event) {
case PlaylistChange playlistChange:
final newState = state!.copyWith(playlist: playlistChange.playlist);
emit(newState);
break;
case LoopPlaylistChange loopPlaylistChange:
final newState =
state!.copyWith(isLooping: loopPlaylistChange.isLooping);
emit(newState);
break;
case PercentPositionChange percentPositionChange:
final newState = state!.copyWith(
currentPercentPosition:
percentPositionChange.currentPercentPosition,
);
emit(newState);
break;
case VolumeChange volumeChange:
final newState = state!.copyWith(volume: volumeChange.volume);
emit(newState);
break;
case DurationChange durationChange:
final newState = state!.copyWith(duration: durationChange.duration);
emit(newState);
break;
case PauseChange pauseChange:
final newState = state!.copyWith(isPlaying: !pauseChange.isPaused);
emit(newState);
break;
case MuteChange muteChange:
final newState = state!.copyWith(isMuted: muteChange.isMuted);
emit(newState);
break;
case TrackListChange trackListChange:
final newState =
state!.copyWith(subtitleTracks: trackListChange.tracks);
emit(newState);
break;
case DemuxerCacheStateChange demuxerCacheStateChange:
final newState = state!.copyWith(
cachedTimestamp: demuxerCacheStateChange.cachedTimestamp,
);
emit(newState);
break;
case PausedForCacheChange pausedForCacheChange:
final newState = state!.copyWith(
isPausedForCache: pausedForCacheChange.isPausedForCache,
);
emit(newState);
break;
}
});
}
}