Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

External stream #174

Merged
merged 10 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 22 additions & 13 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.bluecherrydvr">
<application
<application
android:label="Bluecherry"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
Expand All @@ -18,16 +18,25 @@
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK"/>
<category android:name="android.intent.category.DEFAULT"/>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- https://developer.android.com/training/app-links/verify-android-applinks -->
<!-- Make sure you explicitly set android:autoVerify to "true". -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="rtsp" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
Expand All @@ -36,9 +45,9 @@
android:name="flutterEmbedding"
android:value="2" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_linked_camera" />
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_linked_camera" />
</application>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
</manifest>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest>
2 changes: 1 addition & 1 deletion lib/l10n/app_pt.arb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"@@locale": "pt",
"welcome": "Bem vindo!",
"welcomeDescription": "Bem vindo ao Bluecherry Surveillance DVR!\nVamos conectar ao seu servidor DVR em un instante.",
"welcomeDescription": "Sea bem vindo ao Bluecherry Surveillance DVR!\nVamos conectar ao seu servidor DVR em um instante!",
"configure": "Configure um Servidor DVR",
"configureDescription": "Configure uma conexão com seu servidor DVR remoto",
"hostname": "Hostname",
Expand Down
25 changes: 25 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import 'package:bluecherry_client/firebase_messaging_background_handler.dart';
import 'package:bluecherry_client/models/device.dart';
import 'package:bluecherry_client/models/event.dart';
import 'package:bluecherry_client/models/layout.dart';
import 'package:bluecherry_client/models/server.dart';
import 'package:bluecherry_client/providers/desktop_view_provider.dart';
import 'package:bluecherry_client/providers/downloads_provider.dart';
import 'package:bluecherry_client/providers/events_playback_provider.dart';
Expand All @@ -33,6 +34,7 @@ import 'package:bluecherry_client/providers/mobile_view_provider.dart';
import 'package:bluecherry_client/providers/server_provider.dart';
import 'package:bluecherry_client/providers/settings_provider.dart';
import 'package:bluecherry_client/providers/update_provider.dart';
import 'package:bluecherry_client/utils/app_links.dart' as app_links;
import 'package:bluecherry_client/utils/methods.dart';
import 'package:bluecherry_client/utils/storage.dart';
import 'package:bluecherry_client/utils/theme.dart';
Expand Down Expand Up @@ -162,6 +164,10 @@ class _UnityAppState extends State<UnityApp> with WidgetsBindingObserver {
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);

app_links.register('rtsp');
app_links.init();
app_links.listen();
}

@override
Expand Down Expand Up @@ -292,6 +298,25 @@ class _UnityAppState extends State<UnityApp> with WidgetsBindingObserver {
);
}

if (settings.name == '/rtsp') {
final url = settings.arguments as String;
return MaterialPageRoute(
settings: RouteSettings(
name: '/rtsp',
arguments: url,
),
builder: (context) {
return LivePlayer.fromUrl(
url: url,
device: Device.dump(
name: 'External stream',
url: url,
)..server = Server.dump(name: url),
);
},
);
}

return null;
},
),
Expand Down
17 changes: 14 additions & 3 deletions lib/models/device.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import 'dart:convert';
import 'package:bluecherry_client/models/server.dart';
import 'package:bluecherry_client/providers/server_provider.dart';
import 'package:bluecherry_client/utils/extensions.dart';
import 'package:bluecherry_client/widgets/device_grid/desktop/external_stream.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;

Expand Down Expand Up @@ -50,6 +51,8 @@ class Device {

final String? url;

final MatrixType matrixType;

/// Creates a device.
Device(
this.name,
Expand All @@ -60,16 +63,18 @@ class Device {
this.server, {
this.hasPTZ = false,
this.url,
this.matrixType = MatrixType.t16,
});

Device.dump({
this.name = 'device',
this.id = 0,
this.id = -1,
this.status = true,
this.resolutionX = 640,
this.resolutionY = 480,
this.hasPTZ = false,
this.url,
this.matrixType = MatrixType.t16,
}) : server = Server.dump();

String get uri => 'live/$id';
Expand Down Expand Up @@ -220,7 +225,8 @@ class Device {
resolutionX == other.resolutionX &&
resolutionY == other.resolutionY &&
hasPTZ == other.hasPTZ &&
url == other.url;
url == other.url &&
matrixType == other.matrixType;
}

@override
Expand All @@ -231,7 +237,8 @@ class Device {
resolutionX.hashCode ^
resolutionY.hashCode ^
hasPTZ.hashCode ^
url.hashCode;
url.hashCode ^
matrixType.hashCode;

Device copyWith({
String? name,
Expand All @@ -242,6 +249,7 @@ class Device {
Server? server,
bool? hasPTZ,
String? url,
MatrixType? matrixType,
}) =>
Device(
name ?? this.name,
Expand All @@ -252,6 +260,7 @@ class Device {
server ?? this.server,
hasPTZ: hasPTZ ?? this.hasPTZ,
url: url ?? this.url,
matrixType: matrixType ?? this.matrixType,
);

Map<String, dynamic> toJson() {
Expand All @@ -264,6 +273,7 @@ class Device {
'server': server.toJson(devices: false),
'hasPTZ': hasPTZ,
'url': url,
'matrixType': matrixType.index,
};
}

Expand All @@ -280,6 +290,7 @@ class Device {
Server.fromJson(json['server'] as Map<String, dynamic>),
hasPTZ: json['hasPTZ'] ?? false,
url: json['url'],
matrixType: MatrixType.values[json['matrixType'] ?? 0],
);
}
}
21 changes: 12 additions & 9 deletions lib/providers/desktop_view_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,27 +99,28 @@ class DesktopViewProvider extends ChangeNotifier {
}

/// Adds [device] to the current layout
Future<void> add(Device device) {
Future<void> add(Device device, [Layout? layout]) {
assert(
!currentLayout.devices.contains(device),
'The device is already in the layout',
);

assert(device.status, 'The device must be online');

if (!currentLayout.devices.contains(device)) {
layout ??= currentLayout;

if (!layout.devices.contains(device)) {
// If it's a single view layout, ensure the player will be disposed
// properly before adding one
if (currentLayout.type == DesktopLayoutType.singleView) {
var previousDevice = currentLayout.devices.firstOrNull;
if (layout.type == DesktopLayoutType.singleView) {
var previousDevice = layout.devices.firstOrNull;
if (previousDevice != null) {
currentLayout.devices.clear();
layout.devices.clear();
_releaseDevice(device);
}
}

UnityPlayers.players[device.uuid] ??= UnityPlayers.forDevice(device);
currentLayout.devices.add(device);
layout.devices.add(device);
debugPrint('Added $device');

notifyListeners();
Expand Down Expand Up @@ -178,15 +179,17 @@ class DesktopViewProvider extends ChangeNotifier {
}

/// Adds a new layout
Future<void> addLayout(Layout layout) {
Future<int> addLayout(Layout layout) async {
if (!layouts.contains(layout)) {
debugPrint('Added $layout');
layouts.add(layout);
} else {
debugPrint('$layout already exists');
}
notifyListeners();
return _save(notifyListeners: false);
await _save(notifyListeners: false);

return layouts.indexOf(layout);
}

/// Deletes [layout]
Expand Down
93 changes: 93 additions & 0 deletions lib/utils/app_links.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* This file is a part of Bluecherry Client (https://github.com/bluecherrydvr/unity).
*
* Copyright 2022 Bluecherry, LLC
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import 'dart:io';

import 'package:app_links/app_links.dart';
import 'package:bluecherry_client/main.dart';
import 'package:bluecherry_client/utils/methods.dart';
import 'package:bluecherry_client/widgets/device_grid/desktop/external_stream.dart';
import 'package:flutter/widgets.dart';
import 'package:win32_registry/win32_registry.dart';

final instance = AppLinks();

/// Registers a scheme the app will listen.
Future<void> register(String scheme) async {
if (Platform.isWindows) {
var appPath = Platform.resolvedExecutable;

var protocolRegKey = 'Software\\Classes\\$scheme';
var protocolRegValue = const RegistryValue(
'URL Protocol',
RegistryValueType.string,
'',
);
var protocolCmdRegKey = 'shell\\open\\command';
var protocolCmdRegValue = RegistryValue(
'',
RegistryValueType.string,
'"$appPath" "%1"',
);

Registry.currentUser.createKey(protocolRegKey)
..createValue(protocolRegValue)
..createKey(protocolCmdRegKey).createValue(protocolCmdRegValue);
}
}

/// Initialize the app links.
///
/// When the app is opened from a link, it will open the [AddExternalStreamDialog]
/// as soon as the application is ready.
Future<void> init() async {
try {
var initialUri = await instance.getInitialAppLinkString();
debugPrint('Initial URI: $initialUri');
if (initialUri != null) {
final url = initialUri;
WidgetsBinding.instance.addPostFrameCallback((_) {
final context = navigatorKey.currentContext;
if (context != null && context.mounted) {
AddExternalStreamDialog.addStream(context, url);
}
});
}
} catch (e) {
debugPrint('Error initializing app links: $e');
}
}

/// Listens to any links received while the app is running.
void listen() {
instance.allUriLinkStream.listen((uri) {
debugPrint('Received URI: $uri');
final url = uri.toString();
if (isDesktopPlatform) {
final context = navigatorKey.currentContext;
if (context != null) {
AddExternalStreamDialog.addStream(context, url);
}
} else {
final navigator = navigatorKey.currentState;
if (navigator == null) return;
navigator.pushNamed('/rtsp', arguments: url);
}
});
}
2 changes: 1 addition & 1 deletion lib/widgets/device_grid/desktop/desktop_device_grid.dart
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ class _DesktopTileViewportState extends State<DesktopTileViewport> {
final states = HoverButton.of(context).states;

return Stack(children: [
const Positioned.fill(child: MulticastViewport()),
Positioned.fill(child: MulticastViewport(device: widget.device)),
if (error != null)
Positioned.fill(child: ErrorWarning(message: error)),
IgnorePointer(
Expand Down
Loading