viewer: fixed layout & minimap for videos with non-square pixels
This commit is contained in:
parent
e9de80f887
commit
8601966de4
5 changed files with 62 additions and 27 deletions
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
builder: (context, child) {
|
||||
Widget _builder(Size displaySize) => CustomPaint(
|
||||
painter: MinimapPainter(
|
||||
viewportSize: viewportSize,
|
||||
entrySize: entry.displaySize,
|
||||
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);
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -37,6 +37,8 @@ abstract class AvesVideoController {
|
|||
|
||||
Stream<int> get positionStream;
|
||||
|
||||
ValueNotifier<double> get sarNotifier;
|
||||
|
||||
Widget buildPlayerWidget(BuildContext context);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -193,12 +193,17 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
_buildMagnifier(
|
||||
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,
|
||||
|
|
Loading…
Reference in a new issue