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);
}
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();
DateTime? _bestDate;

View file

@ -1,9 +1,11 @@
import 'dart:math';
import 'package:aves/model/entry.dart';
import 'package:aves/widgets/viewer/video/conductor.dart';
import 'package:aves/widgets/viewer/visual/state.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Minimap extends StatelessWidget {
final AvesEntry entry;
@ -28,16 +30,30 @@ class Minimap extends StatelessWidget {
if (viewportSize == null) return const SizedBox.shrink();
return AnimatedBuilder(
animation: entry.imageChangeNotifier,
builder: (context, child) => CustomPaint(
painter: MinimapPainter(
viewportSize: viewportSize,
entrySize: entry.displaySize,
viewCenterOffset: viewState.position,
viewScale: viewState.scale!,
minimapBorderColor: Colors.white30,
),
size: size,
),
builder: (context, child) {
Widget _builder(Size displaySize) => CustomPaint(
painter: MinimapPainter(
viewportSize: viewportSize,
entrySize: displaySize,
viewCenterOffset: viewState.position,
viewScale: viewState.scale!,
minimapBorderColor: Colors.white30,
),
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;
ValueNotifier<double> get sarNotifier;
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/widgets/viewer/video/controller.dart';
import 'package:collection/collection.dart';
// ignore: import_of_legacy_library_into_null_safe
import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:tuple/tuple.dart';
class IjkPlayerAvesVideoController extends AvesVideoController {
late FijkPlayer _instance;
@ -25,9 +25,11 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
final ValueNotifier<StreamSummary?> _selectedVideoStream = ValueNotifier(null);
final ValueNotifier<StreamSummary?> _selectedAudioStream = ValueNotifier(null);
final ValueNotifier<StreamSummary?> _selectedTextStream = ValueNotifier(null);
final ValueNotifier<Tuple2<int, int>> _sar = ValueNotifier(const Tuple2(1, 1));
Timer? _initialPlayTimer;
@override
final ValueNotifier<double> sarNotifier = ValueNotifier(1);
Stream<FijkValue> get _valueStream => _valueStreamController.stream;
static const initialPlayDelay = Duration(milliseconds: 100);
@ -59,7 +61,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
}
Future<void> _init({int startMillis = 0}) async {
_sar.value = const Tuple2(1, 1);
sarNotifier.value = 1;
_applyOptions(startMillis);
// 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) {
final num = streamInfo[Keys.sarNum] ?? 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
Widget buildPlayerWidget(BuildContext context) {
return ValueListenableBuilder<Tuple2<int, int>>(
valueListenable: _sar,
return ValueListenableBuilder<double>(
valueListenable: sarNotifier,
builder: (context, sar, child) {
final sarNum = sar.item1;
final sarDen = sar.item2;
// 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
final dar = entry.displayAspectRatio * sarNum / sarDen;
// TODO TLAD notify SAR to make the magnifier and minimap use the rendering DAR instead of entry DAR
final dar = entry.displayAspectRatio * sar;
return FijkView(
player: _instance,
fit: FijkFit(

View file

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