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: 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
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import android.os.Handler
|
|||
import android.os.Looper
|
||||
import com.otaliastudios.transcoder.Transcoder
|
||||
import com.otaliastudios.transcoder.TranscoderListener
|
||||
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy
|
||||
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
|
||||
import com.otaliastudios.transcoder.strategy.PassThroughTrackStrategy
|
||||
import com.otaliastudios.transcoder.strategy.TrackStrategy
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
|
@ -17,11 +17,6 @@ object VideoCompressionChannel {
|
|||
|
||||
// Compression parameters defined natively (as requested)
|
||||
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) {
|
||||
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)
|
||||
if (method.name == "createOutputFormat" && result is MediaFormat) {
|
||||
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
|
||||
} as TrackStrategy
|
||||
|
|
@ -61,13 +64,7 @@ object VideoCompressionChannel {
|
|||
Transcoder.into(outputPath)
|
||||
.addDataSource(inputPath)
|
||||
.setVideoTrackStrategy(hevcStrategy)
|
||||
.setAudioTrackStrategy(
|
||||
DefaultAudioStrategy.builder()
|
||||
.channels(AUDIO_CHANNELS)
|
||||
.sampleRate(AUDIO_SAMPLE_RATE)
|
||||
.bitRate(AUDIO_BITRATE)
|
||||
.build()
|
||||
)
|
||||
.setAudioTrackStrategy(PassThroughTrackStrategy())
|
||||
.setListener(object : TranscoderListener {
|
||||
override fun onTranscodeProgress(progress: Double) {
|
||||
mainHandler.post {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,13 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
.go();
|
||||
}
|
||||
|
||||
Future<void> deleteReceiptForUser(int contactId) async {
|
||||
await (delete(receipts)..where(
|
||||
(t) => t.contactId.equals(contactId),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
Future<void> purgeReceivedReceipts() async {
|
||||
await (delete(receivedReceipts)..where(
|
||||
(t) => (t.createdAt.isSmallerThanValue(
|
||||
|
|
|
|||
|
|
@ -1760,5 +1760,5 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
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;
|
||||
}
|
||||
|
||||
Log.info('Uploading $receiptId');
|
||||
|
||||
final message = pb.Message.fromBuffer(receipt.message)
|
||||
..receiptId = receiptId;
|
||||
|
||||
|
|
@ -110,6 +108,8 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
|||
encryptedContent,
|
||||
);
|
||||
|
||||
Log.info('Uploading $receiptId. (${pushNotification?.kind})');
|
||||
|
||||
Uint8List? pushData;
|
||||
if (pushNotification != null && receipt.retryCount <= 1) {
|
||||
// Only show the push notification the first two time.
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
|
||||
if (gUser.typingIndicators) {
|
||||
unawaited(sendTypingIndication(widget.groupId, false));
|
||||
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 8), (
|
||||
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 5), (
|
||||
_,
|
||||
) async {
|
||||
await sendTypingIndication(widget.groupId, false);
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||
}
|
||||
widget.textFieldFocus.addListener(_handleTextFocusChange);
|
||||
if (gUser.typingIndicators) {
|
||||
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 5), (
|
||||
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 1), (
|
||||
_,
|
||||
) async {
|
||||
if (widget.textFieldFocus.hasFocus) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
|
|
@ -94,22 +95,24 @@ class _TypingIndicatorState extends State<TypingIndicator>
|
|||
|
||||
bool isTyping(GroupMember member) {
|
||||
return member.lastTypeIndicator != null &&
|
||||
DateTime.now()
|
||||
clock
|
||||
.now()
|
||||
.difference(
|
||||
member.lastTypeIndicator!,
|
||||
)
|
||||
.inSeconds <=
|
||||
12;
|
||||
2;
|
||||
}
|
||||
|
||||
bool hasChatOpen(GroupMember member) {
|
||||
return member.lastChatOpened != null &&
|
||||
DateTime.now()
|
||||
clock
|
||||
.now()
|
||||
.difference(
|
||||
member.lastChatOpened!,
|
||||
)
|
||||
.inSeconds <=
|
||||
12;
|
||||
8;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@ import 'package:flutter/material.dart';
|
|||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:hashlib/random.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/model/protobuf/client/generated/messages.pb.dart'
|
||||
as pb;
|
||||
import 'package:twonly/src/services/api/messages.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 {
|
||||
const RetransmissionDataView({super.key});
|
||||
|
|
@ -49,6 +52,10 @@ class _RetransmissionDataViewState extends State<RetransmissionDataView> {
|
|||
StreamSubscription<List<Contact>>? subscriptionContacts;
|
||||
List<RetransMsg> messages = [];
|
||||
|
||||
Map<int, int> _contactCount = {};
|
||||
|
||||
int? _filterForUserId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -77,16 +84,39 @@ class _RetransmissionDataViewState extends State<RetransmissionDataView> {
|
|||
subscriptionRetransmission = twonlyDB.receiptsDao.watchAll().listen((
|
||||
updated,
|
||||
) {
|
||||
retransmissions = updated;
|
||||
retransmissions = updated.reversed.toList();
|
||||
if (contacts.isNotEmpty) {
|
||||
messages = RetransMsg.fromRaw(retransmissions, contacts);
|
||||
}
|
||||
_contactCount = {};
|
||||
for (final retransmission in updated) {
|
||||
_contactCount[retransmission.contactId] =
|
||||
(_contactCount[retransmission.contactId] ?? 0) + 1;
|
||||
}
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
var messagesToShow = messages;
|
||||
if (_filterForUserId != null) {
|
||||
messagesToShow = messagesToShow
|
||||
.where((m) => m.contact?.userId == _filterForUserId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Retransmission Database'),
|
||||
|
|
@ -94,69 +124,106 @@ class _RetransmissionDataViewState extends State<RetransmissionDataView> {
|
|||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
reverse: true,
|
||||
children: messages
|
||||
.map(
|
||||
(retrans) => 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),
|
||||
),
|
||||
child: ListView.builder(
|
||||
itemCount: 1 + messagesToShow.length + _contactCount.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return Center(
|
||||
child: FilledButton(
|
||||
onPressed: _filterForUserId == null
|
||||
? null
|
||||
: deleteAllForSelectedUser,
|
||||
child: const Text('Delete all shown entries'),
|
||||
),
|
||||
)
|
||||
.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