mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-04-18 14:22:53 +00:00
fixes android video compression issues
This commit is contained in:
parent
6aab76a47e
commit
840dfed950
9 changed files with 162 additions and 86 deletions
|
|
@ -4,7 +4,9 @@
|
||||||
|
|
||||||
- New: Typing and chat open indicator
|
- New: Typing and chat open indicator
|
||||||
- New: Screen lock for twonly (Can be enabled in the settings.)
|
- New: Screen lock for twonly (Can be enabled in the settings.)
|
||||||
- Fix: Several minor issues with the user interface
|
- Improve: Visual indication when connected to the server
|
||||||
|
- Improve: Several minor issues with the user interface
|
||||||
|
- Fix: Poor audio quality and edge distortions in videos sent from Android
|
||||||
|
|
||||||
## 0.1.3
|
## 0.1.3
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import com.otaliastudios.transcoder.Transcoder
|
import com.otaliastudios.transcoder.Transcoder
|
||||||
import com.otaliastudios.transcoder.TranscoderListener
|
import com.otaliastudios.transcoder.TranscoderListener
|
||||||
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy
|
|
||||||
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
|
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
|
||||||
|
import com.otaliastudios.transcoder.strategy.PassThroughTrackStrategy
|
||||||
import com.otaliastudios.transcoder.strategy.TrackStrategy
|
import com.otaliastudios.transcoder.strategy.TrackStrategy
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
@ -17,11 +17,6 @@ object VideoCompressionChannel {
|
||||||
|
|
||||||
// Compression parameters defined natively (as requested)
|
// Compression parameters defined natively (as requested)
|
||||||
private const val VIDEO_BITRATE = 2_000_000L // 2 Mbps
|
private const val VIDEO_BITRATE = 2_000_000L // 2 Mbps
|
||||||
|
|
||||||
// Audio parameters defined natively
|
|
||||||
private const val AUDIO_BITRATE = 128_000L // 128 kbps
|
|
||||||
private const val AUDIO_SAMPLE_RATE = 44_100
|
|
||||||
private const val AUDIO_CHANNELS = 2
|
|
||||||
|
|
||||||
fun configure(flutterEngine: FlutterEngine, context: Context) {
|
fun configure(flutterEngine: FlutterEngine, context: Context) {
|
||||||
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
||||||
|
|
@ -54,6 +49,14 @@ object VideoCompressionChannel {
|
||||||
val result = if (args != null) method.invoke(baseVideoStrategy, *args) else method.invoke(baseVideoStrategy)
|
val result = if (args != null) method.invoke(baseVideoStrategy, *args) else method.invoke(baseVideoStrategy)
|
||||||
if (method.name == "createOutputFormat" && result is MediaFormat) {
|
if (method.name == "createOutputFormat" && result is MediaFormat) {
|
||||||
result.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_HEVC)
|
result.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_HEVC)
|
||||||
|
|
||||||
|
if (result.containsKey(MediaFormat.KEY_WIDTH) && result.containsKey(MediaFormat.KEY_HEIGHT)) {
|
||||||
|
val width = result.getInteger(MediaFormat.KEY_WIDTH)
|
||||||
|
val height = result.getInteger(MediaFormat.KEY_HEIGHT)
|
||||||
|
// Align dimensions to a multiple of 16 to prevent edge artifacts (green lines/distortions)
|
||||||
|
result.setInteger(MediaFormat.KEY_WIDTH, width - (width % 16))
|
||||||
|
result.setInteger(MediaFormat.KEY_HEIGHT, height - (height % 16))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
} as TrackStrategy
|
} as TrackStrategy
|
||||||
|
|
@ -61,13 +64,7 @@ object VideoCompressionChannel {
|
||||||
Transcoder.into(outputPath)
|
Transcoder.into(outputPath)
|
||||||
.addDataSource(inputPath)
|
.addDataSource(inputPath)
|
||||||
.setVideoTrackStrategy(hevcStrategy)
|
.setVideoTrackStrategy(hevcStrategy)
|
||||||
.setAudioTrackStrategy(
|
.setAudioTrackStrategy(PassThroughTrackStrategy())
|
||||||
DefaultAudioStrategy.builder()
|
|
||||||
.channels(AUDIO_CHANNELS)
|
|
||||||
.sampleRate(AUDIO_SAMPLE_RATE)
|
|
||||||
.bitRate(AUDIO_BITRATE)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.setListener(object : TranscoderListener {
|
.setListener(object : TranscoderListener {
|
||||||
override fun onTranscodeProgress(progress: Double) {
|
override fun onTranscodeProgress(progress: Double) {
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,13 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
||||||
.go();
|
.go();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> deleteReceiptForUser(int contactId) async {
|
||||||
|
await (delete(receipts)..where(
|
||||||
|
(t) => t.contactId.equals(contactId),
|
||||||
|
))
|
||||||
|
.go();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> purgeReceivedReceipts() async {
|
Future<void> purgeReceivedReceipts() async {
|
||||||
await (delete(receivedReceipts)..where(
|
await (delete(receivedReceipts)..where(
|
||||||
(t) => (t.createdAt.isSmallerThanValue(
|
(t) => (t.createdAt.isSmallerThanValue(
|
||||||
|
|
|
||||||
|
|
@ -1760,5 +1760,5 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsTypingIndicationSubtitle =>
|
String get settingsTypingIndicationSubtitle =>
|
||||||
'Bei deaktiviertem Tipp-Indikatoren kannst du nicht sehen, wenn andere gerade eine Nachricht tippen.';
|
'Bei deaktivierten Tipp-Indikatoren kannst du nicht sehen, wenn andere gerade eine Nachricht tippen.';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,8 +95,6 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.info('Uploading $receiptId');
|
|
||||||
|
|
||||||
final message = pb.Message.fromBuffer(receipt.message)
|
final message = pb.Message.fromBuffer(receipt.message)
|
||||||
..receiptId = receiptId;
|
..receiptId = receiptId;
|
||||||
|
|
||||||
|
|
@ -110,6 +108,8 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||||
encryptedContent,
|
encryptedContent,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Log.info('Uploading $receiptId. (${pushNotification?.kind})');
|
||||||
|
|
||||||
Uint8List? pushData;
|
Uint8List? pushData;
|
||||||
if (pushNotification != null && receipt.retryCount <= 1) {
|
if (pushNotification != null && receipt.retryCount <= 1) {
|
||||||
// Only show the push notification the first two time.
|
// Only show the push notification the first two time.
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
|
|
||||||
if (gUser.typingIndicators) {
|
if (gUser.typingIndicators) {
|
||||||
unawaited(sendTypingIndication(widget.groupId, false));
|
unawaited(sendTypingIndication(widget.groupId, false));
|
||||||
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 8), (
|
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 5), (
|
||||||
_,
|
_,
|
||||||
) async {
|
) async {
|
||||||
await sendTypingIndication(widget.groupId, false);
|
await sendTypingIndication(widget.groupId, false);
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
}
|
}
|
||||||
widget.textFieldFocus.addListener(_handleTextFocusChange);
|
widget.textFieldFocus.addListener(_handleTextFocusChange);
|
||||||
if (gUser.typingIndicators) {
|
if (gUser.typingIndicators) {
|
||||||
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 5), (
|
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 1), (
|
||||||
_,
|
_,
|
||||||
) async {
|
) async {
|
||||||
if (widget.textFieldFocus.hasFocus) {
|
if (widget.textFieldFocus.hasFocus) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:clock/clock.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
|
|
@ -94,22 +95,24 @@ class _TypingIndicatorState extends State<TypingIndicator>
|
||||||
|
|
||||||
bool isTyping(GroupMember member) {
|
bool isTyping(GroupMember member) {
|
||||||
return member.lastTypeIndicator != null &&
|
return member.lastTypeIndicator != null &&
|
||||||
DateTime.now()
|
clock
|
||||||
|
.now()
|
||||||
.difference(
|
.difference(
|
||||||
member.lastTypeIndicator!,
|
member.lastTypeIndicator!,
|
||||||
)
|
)
|
||||||
.inSeconds <=
|
.inSeconds <=
|
||||||
12;
|
2;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasChatOpen(GroupMember member) {
|
bool hasChatOpen(GroupMember member) {
|
||||||
return member.lastChatOpened != null &&
|
return member.lastChatOpened != null &&
|
||||||
DateTime.now()
|
clock
|
||||||
|
.now()
|
||||||
.difference(
|
.difference(
|
||||||
member.lastChatOpened!,
|
member.lastChatOpened!,
|
||||||
)
|
)
|
||||||
.inSeconds <=
|
.inSeconds <=
|
||||||
12;
|
8;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,14 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:hashlib/random.dart';
|
import 'package:hashlib/random.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
||||||
as pb;
|
as pb;
|
||||||
import 'package:twonly/src/services/api/messages.dart';
|
import 'package:twonly/src/services/api/messages.dart';
|
||||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
||||||
|
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||||
|
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||||
|
|
||||||
class RetransmissionDataView extends StatefulWidget {
|
class RetransmissionDataView extends StatefulWidget {
|
||||||
const RetransmissionDataView({super.key});
|
const RetransmissionDataView({super.key});
|
||||||
|
|
@ -49,6 +52,10 @@ class _RetransmissionDataViewState extends State<RetransmissionDataView> {
|
||||||
StreamSubscription<List<Contact>>? subscriptionContacts;
|
StreamSubscription<List<Contact>>? subscriptionContacts;
|
||||||
List<RetransMsg> messages = [];
|
List<RetransMsg> messages = [];
|
||||||
|
|
||||||
|
Map<int, int> _contactCount = {};
|
||||||
|
|
||||||
|
int? _filterForUserId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -77,16 +84,39 @@ class _RetransmissionDataViewState extends State<RetransmissionDataView> {
|
||||||
subscriptionRetransmission = twonlyDB.receiptsDao.watchAll().listen((
|
subscriptionRetransmission = twonlyDB.receiptsDao.watchAll().listen((
|
||||||
updated,
|
updated,
|
||||||
) {
|
) {
|
||||||
retransmissions = updated;
|
retransmissions = updated.reversed.toList();
|
||||||
if (contacts.isNotEmpty) {
|
if (contacts.isNotEmpty) {
|
||||||
messages = RetransMsg.fromRaw(retransmissions, contacts);
|
messages = RetransMsg.fromRaw(retransmissions, contacts);
|
||||||
}
|
}
|
||||||
|
_contactCount = {};
|
||||||
|
for (final retransmission in updated) {
|
||||||
|
_contactCount[retransmission.contactId] =
|
||||||
|
(_contactCount[retransmission.contactId] ?? 0) + 1;
|
||||||
|
}
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> deleteAllForSelectedUser() async {
|
||||||
|
final ok = await showAlertDialog(
|
||||||
|
context,
|
||||||
|
'Sure?',
|
||||||
|
'This will delete all retransmission messages for ${contacts[_filterForUserId!]!.username}',
|
||||||
|
);
|
||||||
|
if (ok) {
|
||||||
|
await twonlyDB.receiptsDao.deleteReceiptForUser(_filterForUserId!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var messagesToShow = messages;
|
||||||
|
if (_filterForUserId != null) {
|
||||||
|
messagesToShow = messagesToShow
|
||||||
|
.where((m) => m.contact?.userId == _filterForUserId)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Retransmission Database'),
|
title: const Text('Retransmission Database'),
|
||||||
|
|
@ -94,69 +124,106 @@ class _RetransmissionDataViewState extends State<RetransmissionDataView> {
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView(
|
child: ListView.builder(
|
||||||
reverse: true,
|
itemCount: 1 + messagesToShow.length + _contactCount.length,
|
||||||
children: messages
|
itemBuilder: (context, index) {
|
||||||
.map(
|
if (index == 0) {
|
||||||
(retrans) => ListTile(
|
return Center(
|
||||||
title: Text(
|
child: FilledButton(
|
||||||
retrans.receipt.receiptId,
|
onPressed: _filterForUserId == null
|
||||||
),
|
? null
|
||||||
subtitle: Column(
|
: deleteAllForSelectedUser,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: const Text('Delete all shown entries'),
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'To ${retrans.contact?.username}',
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'Server-Ack: ${retrans.receipt.ackByServerAt}',
|
|
||||||
),
|
|
||||||
if (retrans.receipt.messageId != null)
|
|
||||||
Text(
|
|
||||||
'MessageId: ${retrans.receipt.messageId}',
|
|
||||||
),
|
|
||||||
if (retrans.receipt.messageId != null)
|
|
||||||
FutureBuilder(
|
|
||||||
future: getPushNotificationFromEncryptedContent(
|
|
||||||
retrans.receipt.contactId,
|
|
||||||
retrans.receipt.messageId,
|
|
||||||
pb.EncryptedContent.fromBuffer(
|
|
||||||
pb.Message.fromBuffer(
|
|
||||||
retrans.receipt.message,
|
|
||||||
).encryptedContent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
builder: (d, a) {
|
|
||||||
if (!a.hasData) return Container();
|
|
||||||
return Text(
|
|
||||||
'PushKind: ${a.data?.kind}',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
trailing: FilledButton.icon(
|
|
||||||
onPressed: () async {
|
|
||||||
final newReceiptId = uuid.v4();
|
|
||||||
await twonlyDB.receiptsDao.updateReceipt(
|
|
||||||
retrans.receipt.receiptId,
|
|
||||||
ReceiptsCompanion(
|
|
||||||
receiptId: Value(newReceiptId),
|
|
||||||
ackByServerAt: const Value(null),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await tryToSendCompleteMessage(
|
|
||||||
receiptId: newReceiptId,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
label: const FaIcon(FontAwesomeIcons.arrowRotateRight),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
.toList(),
|
}
|
||||||
|
index -= 1;
|
||||||
|
if (index < _contactCount.length) {
|
||||||
|
final contact = contacts[_contactCount.keys.elementAt(index)];
|
||||||
|
if (contact == null) return Container();
|
||||||
|
return ListTile(
|
||||||
|
leading: AvatarIcon(
|
||||||
|
contactId: contact.userId,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
getContactDisplayName(contact),
|
||||||
|
),
|
||||||
|
trailing: Text(
|
||||||
|
_contactCount.values.elementAt(index).toString(),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (_filterForUserId == contact.userId) {
|
||||||
|
setState(() {
|
||||||
|
_filterForUserId = null;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_filterForUserId = contact.userId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
index -= _contactCount.length;
|
||||||
|
final retrans = messagesToShow[index];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(
|
||||||
|
retrans.receipt.receiptId,
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'To ${retrans.contact?.username}',
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Server-Ack: ${retrans.receipt.ackByServerAt}',
|
||||||
|
),
|
||||||
|
if (retrans.receipt.messageId != null)
|
||||||
|
Text(
|
||||||
|
'MessageId: ${retrans.receipt.messageId}',
|
||||||
|
),
|
||||||
|
if (retrans.receipt.messageId != null)
|
||||||
|
FutureBuilder(
|
||||||
|
future: getPushNotificationFromEncryptedContent(
|
||||||
|
retrans.receipt.contactId,
|
||||||
|
retrans.receipt.messageId,
|
||||||
|
pb.EncryptedContent.fromBuffer(
|
||||||
|
pb.Message.fromBuffer(
|
||||||
|
retrans.receipt.message,
|
||||||
|
).encryptedContent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
builder: (d, a) {
|
||||||
|
if (!a.hasData) return Container();
|
||||||
|
return Text(
|
||||||
|
'PushKind: ${a.data?.kind}',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: FilledButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
final newReceiptId = uuid.v4();
|
||||||
|
await twonlyDB.receiptsDao.updateReceipt(
|
||||||
|
retrans.receipt.receiptId,
|
||||||
|
ReceiptsCompanion(
|
||||||
|
receiptId: Value(newReceiptId),
|
||||||
|
ackByServerAt: const Value(null),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tryToSendCompleteMessage(
|
||||||
|
receiptId: newReceiptId,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
label: const FaIcon(FontAwesomeIcons.arrowRotateRight),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue