diff --git a/packages/dive/lib/src/dive_recording_output.dart b/packages/dive/lib/src/dive_recording_output.dart index 474958b..4796d62 100644 --- a/packages/dive/lib/src/dive_recording_output.dart +++ b/packages/dive/lib/src/dive_recording_output.dart @@ -51,10 +51,8 @@ class DiveRecordingOutput { return const DiveOutputRecordingState(); }, name: 'output-recording-provider'); - DiveOutputRecordingState get state => - DiveCore.container.read(provider.notifier).state; - set state(DiveOutputRecordingState newState) => - DiveCore.container.read(provider.notifier).state = newState; + DiveOutputRecordingState get state => DiveCore.container.read(provider.notifier).state; + set state(DiveOutputRecordingState newState) => DiveCore.container.read(provider.notifier).state = newState; DivePointerOutput? _output; Timer? _updateTimer; @@ -64,7 +62,7 @@ class DiveRecordingOutput { stop(); } - void updateFromMap(Map map) { + void updateFromMap(Map map) { final newState = state.copyWith( folder: map['folder'] as String? ?? '', ); @@ -80,27 +78,23 @@ class DiveRecordingOutput { /// Start recording locally at the [filePath] specified. /// "/Users/larry/Movies/dive1.mkv" /// when [appendTimeStamp] is true: - bool start(String filePath, - {String? filename, - bool appendTimeStamp = false, - String extension = 'mkv'}) { + bool start({String? filename, bool appendTimeStamp = false, String extension = 'mkv'}) { if (_output != null) { stop(); } - String outputPath = filePath; + String outputPath = state.folder ?? ''; if (filename != null && appendTimeStamp) { final now = DateTime.now(); final date = DiveFormat.formatterRecordingDate.format(now); final time = DiveFormat.formatterRecordingTime.format(now); final timeFilename = '$filename $date at $time.$extension'; - outputPath = path.join(filePath, timeFilename); + outputPath = path.join(outputPath, timeFilename); } DiveSystemLog.message('DiveRecordingOutput.start at path: $outputPath'); // Create recording service - _output = obslib.recordingOutputCreate( - path: outputPath, outputName: 'tbd', outputType: outputType); + _output = obslib.recordingOutputCreate(path: outputPath, outputName: 'tbd', outputType: outputType); if (_output == null) { DiveSystemLog.error('DiveRecordingOutput.start output create failed'); return false; @@ -110,8 +104,7 @@ class DiveRecordingOutput { final rv = obslib.outputStart(_output!); if (rv) { _updateState(); - _updateTimer = - Timer.periodic(const Duration(seconds: 1), (timer) => _updateState()); + _updateTimer = Timer.periodic(const Duration(seconds: 1), (timer) => _updateState()); } else { DiveSystemLog.error('DiveRecordingOutput.start output start failed'); } @@ -145,18 +138,13 @@ class DiveRecordingOutput { /// Sync the media state from the media source to the state provider. Future _updateState() async { if (_output == null) return; - final activeState = - DiveOutputRecordingActiveState.values[obslib.outputGetState(_output!)]; + final activeState = DiveOutputRecordingActiveState.values[obslib.outputGetState(_output!)]; final currentState = state; var startTime = currentState.startTime; - if (currentState.startTime == null && - activeState == DiveOutputRecordingActiveState.active) { + if (currentState.startTime == null && activeState == DiveOutputRecordingActiveState.active) { startTime = DateTime.now(); } - final duration = startTime != null - ? DateTime.now().difference(startTime) - : Duration.zero; - state = DiveOutputRecordingState( - activeState: activeState, startTime: startTime, duration: duration); + final duration = startTime != null ? DateTime.now().difference(startTime) : Duration.zero; + state = DiveOutputRecordingState(activeState: activeState, startTime: startTime, duration: duration); } } diff --git a/packages/dive_ui/example/lib/dive_caster.dart b/packages/dive_ui/example/lib/dive_caster.dart index 0f80feb..eeb1710 100644 --- a/packages/dive_ui/example/lib/dive_caster.dart +++ b/packages/dive_ui/example/lib/dive_caster.dart @@ -2,6 +2,7 @@ import 'package:dive/dive.dart'; import 'package:dive_ui/dive_caster.dart'; import 'package:dive_ui/dive_ui_widgets.dart'; import 'package:flutter/widgets.dart'; +import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; /// Dive Caster Multi Camera Streaming and Recording @@ -72,6 +73,12 @@ class DiveCasterMain { // Create the recording output final recordingOutput = DiveRecordingOutput(); + recordingOutput.updateFromMap(elementsNode.settings['recording'] as Map? ?? {}); + final recordingState = recordingOutput.state; + if (recordingState.folder == null || recordingState.folder!.isEmpty) { + final folder = path.join(applicationSupportDirectory.path, 'recordings'); + recordingOutput.state = recordingState.copyWith(folder: folder); + } elements.addRecordingOutput(recordingOutput); // Create the streaming output diff --git a/packages/dive_ui/example/lib/main_example15.dart b/packages/dive_ui/example/lib/main_example15.dart index 024d260..46fd2b6 100644 --- a/packages/dive_ui/example/lib/main_example15.dart +++ b/packages/dive_ui/example/lib/main_example15.dart @@ -152,9 +152,10 @@ class _BodyWidgetState extends State { // Create the recording output final recordingOutput = DiveRecordingOutput(); widget.elements.addRecordingOutput(recordingOutput); + recordingOutput.state = recordingOutput.state.copyWith(folder: '/Users/larry/Movies/dive/'); // Start recording. - recordingOutput.start('/Users/larry/Movies/dive/dive1.mkv'); + recordingOutput.start(filename: 'dive1', appendTimeStamp: true); } @override diff --git a/packages/dive_ui/lib/dive_caster.dart b/packages/dive_ui/lib/dive_caster.dart index d4621f3..ba0c524 100644 --- a/packages/dive_ui/lib/dive_caster.dart +++ b/packages/dive_ui/lib/dive_caster.dart @@ -3,7 +3,6 @@ import 'package:dive/dive.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:path/path.dart' as path; import 'dive_record_settings_dialog.dart'; import 'dive_stream_settings_dialog.dart'; @@ -118,13 +117,10 @@ class DiveHeaderRecordButton extends StatelessWidget { String duration = ''; final elementsState = ref.watch(elements.provider); if (elementsState.recordingOutput != null) { - final recordingState = - ref.watch(elementsState.recordingOutput!.provider); - recording = recordingState.activeState == - DiveOutputRecordingActiveState.active; - duration = recordingState.duration != null - ? DiveFormat.formatDuration(recordingState.duration!) - : ''; + final recordingState = ref.watch(elementsState.recordingOutput!.provider); + recording = recordingState.activeState == DiveOutputRecordingActiveState.active; + duration = + recordingState.duration != null ? DiveFormat.formatDuration(recordingState.duration!) : ''; } return DiveHeaderButton( title: recording ? 'RECORDING' : 'RECORD', @@ -133,18 +129,14 @@ class DiveHeaderRecordButton extends StatelessWidget { onPressed: () async { final elementsState = elements.state; if (elementsState.recordingOutput != null) { - final recordingState = - ref.read(elementsState.recordingOutput!.provider); - final recording = recordingState.activeState == - DiveOutputRecordingActiveState.active; + final recordingState = ref.read(elementsState.recordingOutput!.provider); + final recording = recordingState.activeState == DiveOutputRecordingActiveState.active; if (recording) { // Stop recording. elementsState.recordingOutput!.stop(); } else { - final folder = path.join('~/Documents'); // Start recording. - elementsState.recordingOutput! - .start(folder, filename: 'dive1', appendTimeStamp: true); + elementsState.recordingOutput!.start(filename: 'dive1', appendTimeStamp: true); } } }, @@ -166,8 +158,7 @@ class DiveHeaderRecordButton extends StatelessWidget { child: DiveRecordSettingsScreen( saveFolder: recordingOutput.state.folder, useDialog: true, - onApplyCallback: (String directory) => - _onDialogApply(context, directory), + onApplyCallback: (String directory) => _onDialogApply(context, directory), ), ); }); @@ -177,7 +168,10 @@ class DiveHeaderRecordButton extends StatelessWidget { final recordingOutput = elements.state.recordingOutput; if (recordingOutput != null) { recordingOutput.stop(); -// ? recordingOutput. .state.folder = directory; + recordingOutput.state = recordingOutput.state.copyWith(folder: directory); + + // Save the updated settings. + elements.saveAppSettings(); } Navigator.of(context).pop(); } @@ -197,15 +191,11 @@ class DiveHeaderStreamButton extends StatelessWidget { String duration = ''; final elementsState = ref.watch(elements.provider); if (elementsState.streamingOutput != null) { - final streamingState = - ref.watch(elementsState.streamingOutput!.provider); - streaming = streamingState.activeState == - DiveOutputStreamingActiveState.active; - failed = streamingState.activeState == - DiveOutputStreamingActiveState.failed; - duration = streamingState.duration != null - ? DiveFormat.formatDuration(streamingState.duration!) - : ''; + final streamingState = ref.watch(elementsState.streamingOutput!.provider); + streaming = streamingState.activeState == DiveOutputStreamingActiveState.active; + failed = streamingState.activeState == DiveOutputStreamingActiveState.failed; + duration = + streamingState.duration != null ? DiveFormat.formatDuration(streamingState.duration!) : ''; } return DiveHeaderButton( title: streaming ? 'STREAMING' : 'STREAM', @@ -220,10 +210,8 @@ class DiveHeaderStreamButton extends StatelessWidget { onPressed: () { final elementsState = elements.state; if (elementsState.streamingOutput != null) { - final recordingState = - ref.read(elementsState.streamingOutput!.provider); - final active = recordingState.activeState == - DiveOutputStreamingActiveState.active; + final recordingState = ref.read(elementsState.streamingOutput!.provider); + final active = recordingState.activeState == DiveOutputStreamingActiveState.active; if (active) { // Stop streaming. elementsState.streamingOutput!.stop(); @@ -248,34 +236,30 @@ class DiveHeaderStreamButton extends StatelessWidget { builder: (BuildContext context) { return SingleChildScrollView( child: DiveStreamSettingsScreen( - service: elements.state.streamingOutput == null - ? null - : elements.state.streamingOutput!.service, - server: elements.state.streamingOutput == null - ? null - : elements.state.streamingOutput!.server, - serviceKey: elements.state.streamingOutput == null - ? null - : elements.state.streamingOutput!.serviceKey, + service: + elements.state.streamingOutput == null ? null : elements.state.streamingOutput!.service, + server: elements.state.streamingOutput == null ? null : elements.state.streamingOutput!.server, + serviceKey: + elements.state.streamingOutput == null ? null : elements.state.streamingOutput!.serviceKey, useDialog: true, - onApplyCallback: (DiveRTMPService service, DiveRTMPServer server, - String serviceKey) => + onApplyCallback: (DiveRTMPService service, DiveRTMPServer server, String serviceKey) => _onDialogApply(context, service, server, serviceKey), ), ); }); } - void _onDialogApply(BuildContext context, DiveRTMPService service, - DiveRTMPServer server, String serviceKey) { - final state = elements.state; - if (state.streamingOutput != null) { - state.streamingOutput!.stop(); - state.streamingOutput!.service = service; - state.streamingOutput!.server = server; - state.streamingOutput!.serviceUrl = server.url; - state.streamingOutput!.serviceKey = serviceKey; - + void _onDialogApply( + BuildContext context, DiveRTMPService service, DiveRTMPServer server, String serviceKey) { + final streamingOutput = elements.state.streamingOutput; + if (streamingOutput != null) { + streamingOutput.stop(); + streamingOutput.service = service; + streamingOutput.server = server; + streamingOutput.serviceUrl = server.url; + streamingOutput.serviceKey = serviceKey; + + // Save the updated settings. elements.saveAppSettings(); } Navigator.of(context).pop(); @@ -295,8 +279,7 @@ class DiveCasterFooter extends StatelessWidget { height: 40.0, child: Row( children: [ - DiveHeaderIcon( - icon: Icon(Icons.live_tv, color: DiveCasterTheme.textColor)), + DiveHeaderIcon(icon: Icon(Icons.live_tv, color: DiveCasterTheme.textColor)), DiveHeaderText(text: 'Dive Caster'), ], ), @@ -358,13 +341,10 @@ class DiveCasterContentArea extends StatelessWidget { return Consumer( builder: (context, ref, child) { final elementsState = ref.watch(elements.provider); - final controller = elementsState.videoMixes.isNotEmpty - ? elementsState.videoMixes.first.controller - : null; + final controller = + elementsState.videoMixes.isNotEmpty ? elementsState.videoMixes.first.controller : null; return Center( - child: DivePreview( - aspectRatio: DiveCoreAspectRatio.HD.ratio, - controller: controller), + child: DivePreview(aspectRatio: DiveCoreAspectRatio.HD.ratio, controller: controller), ); }, ); @@ -388,8 +368,7 @@ class DiveHeaderClock extends ConsumerWidget { width: 100.0, height: 36, child: Center( - child: Text(nowFormatted, - style: TextStyle(color: DiveCasterTheme.textColor)), + child: Text(nowFormatted, style: TextStyle(color: DiveCasterTheme.textColor)), ), ); } @@ -467,15 +446,12 @@ class _DiveHeaderButtonState extends State { : widget.useRedBackground ? MaterialStatePropertyAll(DiveCasterTheme.headerButtonRedColor) : MaterialStatePropertyAll(DiveCasterTheme.headerBackgroundColor), - foregroundColor: - MaterialStatePropertyAll(DiveCasterTheme.headerButtonTextColor), + foregroundColor: MaterialStatePropertyAll(DiveCasterTheme.headerButtonTextColor), overlayColor: widget.useBlueBackground ? MaterialStatePropertyAll(DiveCasterTheme.headerButtonBlueHoverColor) : widget.useRedBackground - ? MaterialStatePropertyAll( - DiveCasterTheme.headerButtonRedHoverColor) - : MaterialStatePropertyAll( - DiveCasterTheme.headerButtonHoverColor), + ? MaterialStatePropertyAll(DiveCasterTheme.headerButtonRedHoverColor) + : MaterialStatePropertyAll(DiveCasterTheme.headerButtonHoverColor), splashFactory: NoSplash.splashFactory, ); @@ -499,9 +475,7 @@ class _DiveHeaderButtonState extends State { if (widget.onGearPressed != null) IconButton( icon: Icon(DiveUI.iconSet.sourceSettingsButton), - color: _hovering - ? DiveCasterTheme.headerButtonTextColor - : DiveCasterTheme.headerButtonHoverColor, + color: _hovering ? DiveCasterTheme.headerButtonTextColor : DiveCasterTheme.headerButtonHoverColor, onPressed: widget.onGearPressed, ), ], diff --git a/packages/dive_ui/lib/dive_record_settings_dialog.dart b/packages/dive_ui/lib/dive_record_settings_dialog.dart index 583d31f..292209a 100644 --- a/packages/dive_ui/lib/dive_record_settings_dialog.dart +++ b/packages/dive_ui/lib/dive_record_settings_dialog.dart @@ -1,7 +1,9 @@ // Copyright (c) 2023 Larry Aasen. All rights reserved. import 'package:dive/dive.dart'; +import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; + import 'dive_ui.dart'; /// An icon button that presents the record settings dialog. @@ -96,13 +98,14 @@ class _DiveRecordSettingsScreenState extends State { Widget _buildConfig(BuildContext context) { final position = Padding( - padding: EdgeInsets.only(top: 20, left: 10, right: 10), + padding: EdgeInsets.only(top: 0.0, left: 10, right: 10), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + ElevatedButton(child: Text('Change Folder'), onPressed: () => _onChangeFolder()), Padding(padding: EdgeInsets.only(top: 20), child: Text('Save recordings to this folder:')), - Text(_saveFolder), + Container(constraints: BoxConstraints(maxWidth: 400.0), child: Text(_saveFolder)), ], )); @@ -127,6 +130,14 @@ class _DiveRecordSettingsScreenState extends State { _saveFolder = widget.saveFolder ?? ''; } + void _onChangeFolder() async { + getDirectoryPath(confirmButtonText: 'OK', initialDirectory: '').then((String? path) { + if (path == null) return; + DiveSystemLog.message('DiveRecordSettingsScreen: patj=$path', group: 'dive_ui'); + setState(() => _saveFolder = path); + }); + } + void _onReset() { setState(() { _useInitialState(); diff --git a/packages/dive_ui/pubspec.yaml b/packages/dive_ui/pubspec.yaml index b382e14..ddad8ec 100644 --- a/packages/dive_ui/pubspec.yaml +++ b/packages/dive_ui/pubspec.yaml @@ -45,9 +45,6 @@ dependencies: # From Dart team: path_provider: ^2.0.13 - # From flutter.dev: Flutter plugin for playing back video on a Widget surface. - # video_player: '>=0.10.12+3 <2.0.0' - dev_dependencies: flutter_test: sdk: flutter