viewer: fixed layout & minimap for videos with non-square pixels

This commit is contained in:
Thibault Deckers 2021-06-10 18:45:08 +09:00
parent e9de80f887
commit 8601966de4
5 changed files with 62 additions and 27 deletions

View file

@ -289,6 +289,18 @@ class AvesEntry {
return isRotated ? Size(h, w) : Size(w, h); return isRotated ? Size(h, w) : Size(w, h);
} }
Size videoDisplaySize(double sar) {
final size = displaySize;
if (sar != 1) {
final dar = displayAspectRatio * sar;
final w = size.width;
final h = size.height;
if (w >= h) return Size(w, w / dar);
if (h > w) return Size(h * dar, h);
}
return size;
}
int get megaPixels => (width * height / 1000000).round(); int get megaPixels => (width * height / 1000000).round();
DateTime? _bestDate; DateTime? _bestDate;

View file

@ -1,9 +1,11 @@
import 'dart:math'; import 'dart:math';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/widgets/viewer/video/conductor.dart';
import 'package:aves/widgets/viewer/visual/state.dart'; import 'package:aves/widgets/viewer/visual/state.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Minimap extends StatelessWidget { class Minimap extends StatelessWidget {
final AvesEntry entry; final AvesEntry entry;
@ -28,16 +30,30 @@ class Minimap extends StatelessWidget {
if (viewportSize == null) return const SizedBox.shrink(); if (viewportSize == null) return const SizedBox.shrink();
return AnimatedBuilder( return AnimatedBuilder(
animation: entry.imageChangeNotifier, animation: entry.imageChangeNotifier,
builder: (context, child) => CustomPaint( builder: (context, child) {
Widget _builder(Size displaySize) => CustomPaint(
painter: MinimapPainter( painter: MinimapPainter(
viewportSize: viewportSize, viewportSize: viewportSize,
entrySize: entry.displaySize, entrySize: displaySize,
viewCenterOffset: viewState.position, viewCenterOffset: viewState.position,
viewScale: viewState.scale!, viewScale: viewState.scale!,
minimapBorderColor: Colors.white30, minimapBorderColor: Colors.white30,
), ),
size: size, size: size,
), );
if (entry.isVideo) {
final videoController = context.read<VideoConductor>().getController(entry);
if (videoController == null) return const SizedBox();
return ValueListenableBuilder<double>(
valueListenable: videoController.sarNotifier,
builder: (context, sar, child) {
return _builder(entry.videoDisplaySize(sar));
},
);
}
return _builder(entry.displaySize);
},
); );
}), }),
); );

View file

@ -37,6 +37,8 @@ abstract class AvesVideoController {
Stream<int> get positionStream; Stream<int> get positionStream;
ValueNotifier<double> get sarNotifier;
Widget buildPlayerWidget(BuildContext context); Widget buildPlayerWidget(BuildContext context);
} }

View file

@ -9,11 +9,11 @@ import 'package:aves/model/video/metadata.dart';
import 'package:aves/utils/change_notifier.dart'; import 'package:aves/utils/change_notifier.dart';
import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves/widgets/viewer/video/controller.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
// ignore: import_of_legacy_library_into_null_safe // ignore: import_of_legacy_library_into_null_safe
import 'package:fijkplayer/fijkplayer.dart'; import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tuple/tuple.dart';
class IjkPlayerAvesVideoController extends AvesVideoController { class IjkPlayerAvesVideoController extends AvesVideoController {
late FijkPlayer _instance; late FijkPlayer _instance;
@ -25,9 +25,11 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
final ValueNotifier<StreamSummary?> _selectedVideoStream = ValueNotifier(null); final ValueNotifier<StreamSummary?> _selectedVideoStream = ValueNotifier(null);
final ValueNotifier<StreamSummary?> _selectedAudioStream = ValueNotifier(null); final ValueNotifier<StreamSummary?> _selectedAudioStream = ValueNotifier(null);
final ValueNotifier<StreamSummary?> _selectedTextStream = ValueNotifier(null); final ValueNotifier<StreamSummary?> _selectedTextStream = ValueNotifier(null);
final ValueNotifier<Tuple2<int, int>> _sar = ValueNotifier(const Tuple2(1, 1));
Timer? _initialPlayTimer; Timer? _initialPlayTimer;
@override
final ValueNotifier<double> sarNotifier = ValueNotifier(1);
Stream<FijkValue> get _valueStream => _valueStreamController.stream; Stream<FijkValue> get _valueStream => _valueStreamController.stream;
static const initialPlayDelay = Duration(milliseconds: 100); static const initialPlayDelay = Duration(milliseconds: 100);
@ -59,7 +61,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
} }
Future<void> _init({int startMillis = 0}) async { Future<void> _init({int startMillis = 0}) async {
_sar.value = const Tuple2(1, 1); sarNotifier.value = 1;
_applyOptions(startMillis); _applyOptions(startMillis);
// calling `setDataSource()` with `autoPlay` starts as soon as possible, but often yields initial artifacts // calling `setDataSource()` with `autoPlay` starts as soon as possible, but often yields initial artifacts
@ -165,7 +167,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
if (streamInfo != null) { if (streamInfo != null) {
final num = streamInfo[Keys.sarNum] ?? 0; final num = streamInfo[Keys.sarNum] ?? 0;
final den = streamInfo[Keys.sarDen] ?? 0; final den = streamInfo[Keys.sarDen] ?? 0;
_sar.value = Tuple2(num != 0 ? num : 1, den != 0 ? den : 1); sarNotifier.value = (num != 0 ? num : 1) / (den != 0 ? den : 1);
} }
} }
} }
@ -232,15 +234,12 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
@override @override
Widget buildPlayerWidget(BuildContext context) { Widget buildPlayerWidget(BuildContext context) {
return ValueListenableBuilder<Tuple2<int, int>>( return ValueListenableBuilder<double>(
valueListenable: _sar, valueListenable: sarNotifier,
builder: (context, sar, child) { builder: (context, sar, child) {
final sarNum = sar.item1;
final sarDen = sar.item2;
// derive DAR (Display Aspect Ratio) from SAR (Storage Aspect Ratio), if any // derive DAR (Display Aspect Ratio) from SAR (Storage Aspect Ratio), if any
// e.g. 960x536 (~16:9) with SAR 4:3 should be displayed as ~2.39:1 // e.g. 960x536 (~16:9) with SAR 4:3 should be displayed as ~2.39:1
final dar = entry.displayAspectRatio * sarNum / sarDen; final dar = entry.displayAspectRatio * sar;
// TODO TLAD notify SAR to make the magnifier and minimap use the rendering DAR instead of entry DAR
return FijkView( return FijkView(
player: _instance, player: _instance,
fit: FijkFit( fit: FijkFit(

View file

@ -193,12 +193,17 @@ class _EntryPageViewState extends State<EntryPageView> {
return Stack( return Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
_buildMagnifier( ValueListenableBuilder<double>(
valueListenable: videoController.sarNotifier,
builder: (context, sar, child) {
return _buildMagnifier(
displaySize: entry.videoDisplaySize(sar),
child: VideoView( child: VideoView(
entry: entry, entry: entry,
controller: videoController, controller: videoController,
), ),
), );
}),
// fade out image to ease transition with the player // fade out image to ease transition with the player
StreamBuilder<VideoStatus>( StreamBuilder<VideoStatus>(
stream: videoController.statusStream, stream: videoController.statusStream,
@ -231,13 +236,14 @@ class _EntryPageViewState extends State<EntryPageView> {
ScaleLevel maxScale = maxScale, ScaleLevel maxScale = maxScale,
ScaleStateCycle scaleStateCycle = defaultScaleStateCycle, ScaleStateCycle scaleStateCycle = defaultScaleStateCycle,
bool applyScale = true, bool applyScale = true,
Size? displaySize,
required Widget child, required Widget child,
}) { }) {
return Magnifier( return Magnifier(
// key includes modified date to refresh when the image is modified by metadata (e.g. rotated) // key includes modified date to refresh when the image is modified by metadata (e.g. rotated)
key: ValueKey('${entry.pageId}_${entry.dateModifiedSecs}'), key: ValueKey('${entry.pageId}_${entry.dateModifiedSecs}'),
controller: _magnifierController, controller: _magnifierController,
childSize: entry.displaySize, childSize: displaySize ?? entry.displaySize,
minScale: minScale, minScale: minScale,
maxScale: maxScale, maxScale: maxScale,
initialScale: initialScale, initialScale: initialScale,