Skip to content

Commit

Permalink
end yt stream
Browse files Browse the repository at this point in the history
  • Loading branch information
cp-sidhdhi-p committed Feb 3, 2025
1 parent 8118bd6 commit 936394c
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 27 deletions.
20 changes: 20 additions & 0 deletions data/lib/service/live_stream/live_stream_endpoint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class CreateLiveStreamEndPoint extends Endpoint {
final DateTime scheduledStartTime;
final YouTubeResolution resolution;
final MediumOption option;
final PrivacyStatus? privacyStatus;

CreateLiveStreamEndPoint({
required this.name,
Expand All @@ -16,6 +17,7 @@ class CreateLiveStreamEndPoint extends Endpoint {
required this.scheduledStartTime,
required this.resolution,
required this.option,
this.privacyStatus,
});

@override
Expand All @@ -32,6 +34,7 @@ class CreateLiveStreamEndPoint extends Endpoint {
"scheduledStartTime": scheduledStartTime.toIso8601String(),
"resolution": resolution.stringResolution,
"option": option.name,
"privacyStatus": privacyStatus?.name,
};
}

Expand All @@ -43,6 +46,23 @@ class GetYTChannelEndPoint extends Endpoint {
HttpMethod get method => HttpMethod.get;
}

class EndYTBroadcastEndPoint extends Endpoint {
final String broadcastId;

EndYTBroadcastEndPoint({
required this.broadcastId,
});

@override
String get path => 'liveStream/endYouTubeBroadcast';

@override
HttpMethod get method => HttpMethod.get;

@override
dynamic get data => {"broadcastId": broadcastId};
}

enum YouTubeResolution {
p1080,
p720,
Expand Down
8 changes: 8 additions & 0 deletions data/lib/service/live_stream/live_stream_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,12 @@ class LiveStreamService {
throw AppError.fromError(error, stack);
}
}

Future<void> endYTBroadcast(String broadcastId) async {
try {
await client.req(EndYTBroadcastEndPoint(broadcastId: broadcastId));
} catch (e, stack) {
throw AppError.fromError(e, stack);
}
}
}
17 changes: 9 additions & 8 deletions khelo/assets/images/ic_live_streamer.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions khelo/functions/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ expressApp.post("/liveStream/createYouTubeStream", (req, res) => {
liveStreamService.createYouTubeStream(req, res);
});

expressApp.post("/liveStream/endYouTubeBroadcast", (req, res) => {
liveStreamService.endYouTubeBroadcast(req, res);
});

expressApp.get("/liveStream/getYouTubeChannel", (req, res) => {
liveStreamService.getYouTubeChannel(req, res);
});
Expand Down
67 changes: 65 additions & 2 deletions khelo/functions/src/live_stream/live_stream_service.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ class LiveStreamService {
scheduledStartTime: new Date(data.scheduledStartTime).toISOString(),
},
status: {
privacyStatus: "public",
privacyStatus: data.privacyStatus || "public",
selfDeclaredMadeForKids: false,
},
contentDetails: {
enableContentEncryption: false, // TODO: Set to true if you need encryption
enableContentEncryption: true,
enableDvr: true,
enableMonitorStream: true,
recordFromStart: true,
Expand Down Expand Up @@ -129,6 +129,69 @@ class LiveStreamService {
}
}

async endYouTubeBroadcast(req, res) {
try {
const userId = req.headers["auth-uid"];
if (!userId) {
console.error("AuthService: unauthorized request");
return res.status(400).send("unauthorized");
}

const broadcastId = req.body.broadcastId;
if (!broadcastId) {
console.error("Broadcast ID is required.");
return res.status(400).send("Broadcast ID is required.");
}

const userDoc = await this.userRepository.getUser(userId);
if (!userDoc) {
console.error("User not found");
return res.status(404).send("User not found");
}

const refreshToken = userDoc.google_refresh_token;
if (!refreshToken) {
console.error("No refresh token available for this user.");
return res.status(400).send("No refresh token available.");
}

const oauth2Client = new google.auth.OAuth2(
process.env.WEB_CLIENT_ID,
process.env.WEB_CLIENT_SECRET,
process.env.REDIRECT_URL,
);

oauth2Client.setCredentials({
refresh_token: refreshToken,
});

// Refresh access token
const {credentials} = await oauth2Client.refreshAccessToken();
oauth2Client.setCredentials(credentials);

const youtube = google.youtube({
version: "v3",
auth: oauth2Client,
});

// Transition the broadcast to "complete"
const response = await youtube.liveBroadcasts.transition({
id: broadcastId,
broadcastStatus: "complete",
part: "status",
});

return res.status(200).send({
message: "Broadcast ended successfully.",
broadcastId: response.data.id,
status: response.data.status,
});
} catch (error) {
console.error("Error ending broadcast:", error);
return res.status(400).send(error);
}
}

async getYouTubeChannel(req, res) {
try {
const userId = req.headers["auth-uid"];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class AddStreamInfoViewNotifier extends StateNotifier<AddStreamInfoViewState> {
channelId: state.channelId,
resolution: resolution,
scheduledStartTime: matchStartTime,
privacyStatus: PrivacyStatus.public,
));
state = state.copyWith(
stream: stream,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:data/api/live_stream/live_stream_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:haishin_kit/stream_view_texture.dart';
import 'package:khelo/components/app_page.dart';
import 'package:khelo/components/error_screen.dart';
Expand All @@ -9,6 +10,7 @@ import 'package:style/animations/on_tap_scale.dart';
import 'package:style/extensions/context_extensions.dart';
import 'package:style/text/app_text_style.dart';

import '../../../../../components/error_snackbar.dart';
import '../../../../../domain/extensions/widget_extension.dart';

class StreamCameraScreen extends ConsumerStatefulWidget {
Expand All @@ -34,9 +36,19 @@ class _StreamCameraScreenState extends ConsumerState<StreamCameraScreen> {
Widget build(BuildContext context) {
final state = ref.watch(streamCameraStateProvider);

return AppPage(
body: Builder(
builder: (context) => _body(context, state),
_observeIsPop(context, ref);
_observeActionError(context, ref);

return PopScope(
onPopInvokedWithResult: (didPop, result) {
if (!state.isPop && state.stream?.status == LiveStreamStatus.live) {
notifier.updateStreamingStatus(LiveStreamStatus.paused);
}
},
child: AppPage(
body: Builder(
builder: (context) => _body(context, state),
),
),
);
}
Expand Down Expand Up @@ -175,4 +187,22 @@ class _StreamCameraScreenState extends ConsumerState<StreamCameraScreen> {
),
);
}

void _observeIsPop(BuildContext context, WidgetRef ref) {
ref.listen(streamCameraStateProvider.select((value) => value.isPop),
(previous, next) {
if (next) {
context.pop();
}
});
}

void _observeActionError(BuildContext context, WidgetRef ref) {
ref.listen(streamCameraStateProvider.select((value) => value.actionError),
(previous, next) {
if (next != null) {
showErrorSnackBar(context: context, error: next);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:audio_session/audio_session.dart';
import 'package:data/api/live_stream/live_stream_model.dart';
import 'package:data/errors/app_error.dart';
import 'package:data/service/live_stream/live_stream_service.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Expand Down Expand Up @@ -43,24 +44,46 @@ class StreamCameraViewNotifier extends StateNotifier<StreamCameraViewState> {
switch (status) {
case LiveStreamStatus.live:
state.connection?.connect("${state.stream!.server_url}/");
await state.rtmpStream?.setHasVideo(true);
case LiveStreamStatus.paused:
await state.rtmpStream?.setHasVideo(false);
state.connection?.close();
case LiveStreamStatus.completed:
await state.rtmpStream?.setHasVideo(false);
await state.rtmpStream?.close();
state.connection?.close();
await _detachAudioAndVideo();
await state.rtmpStream?.close();
await endYTBroadcast();
default:
break;
}

state = state.copyWith(stream: state.stream?.copyWith(status: status));
} catch (e) {
state = state.copyWith(actionError: e);
debugPrint(
"StreamCameraViewNotifier: error while updating streaming status -> $e");
}
}

Future<void> endYTBroadcast() async {
if (state.stream == null) return;
try {
await _liveStreamService.endYTBroadcast(state.stream!.broadcast_id);
state = state.copyWith(isPop: true);
} catch (e) {
state = state.copyWith(actionError: e);
debugPrint(
"StreamCameraViewNotifier: error while end yt broadcast -> $e");
}
}

Future<void> _detachAudioAndVideo() async {
if (state.rtmpStream == null) return;

await Future.wait([
state.rtmpStream!.attachAudio(null),
state.rtmpStream!.attachVideo(null),
]);
}

Future<void> requestPermissions() async {
final permissions = [Permission.camera, Permission.microphone];

Expand Down Expand Up @@ -107,33 +130,39 @@ class StreamCameraViewNotifier extends StateNotifier<StreamCameraViewState> {
);
await stream.attachAudio(AudioSource());
await stream.attachVideo(VideoSource(position: state.currentPosition));
await stream.setHasAudio(state.isAudioEnable);
await stream.setHasVideo(state.stream?.status == LiveStreamStatus.live);

state = state.copyWith(
connection: connection,
rtmpStream: stream,
isLoading: false,
);
} catch (e) {
} catch (e, stack) {
debugPrint(
"StreamCameraViewNotifier: error while init platform state -> $e");
state = state.copyWith(error: e, isLoading: false);
state =
state.copyWith(error: AppError.fromError(e, stack), isLoading: false);
}
}

void switchCamera() {
Future<void> switchCamera() async {
state = state.copyWith(
currentPosition: state.currentPosition == CameraPosition.front
? CameraPosition.back
: CameraPosition.front);

state.rtmpStream?.attachVideo(VideoSource(position: state.currentPosition));
await state.rtmpStream
?.attachVideo(VideoSource(position: state.currentPosition));
}

Future<void> toggleMuteButton() async {
final hasAudio = state.isAudioEnable;
state.rtmpStream?.setHasAudio(!hasAudio);
await state.rtmpStream?.setHasAudio(!hasAudio);

// if (hasAudio) {
// await state.rtmpStream?.attachAudio(null);
// } else {
// await state.rtmpStream?.attachAudio(AudioSource());
// }
state = state.copyWith(isAudioEnable: !hasAudio);
}

Expand All @@ -149,11 +178,13 @@ class StreamCameraViewNotifier extends StateNotifier<StreamCameraViewState> {
class StreamCameraViewState with _$StreamCameraViewState {
const factory StreamCameraViewState({
Object? error,
Object? actionError,
LiveStreamModel? stream,
RtmpConnection? connection,
RtmpStream? rtmpStream,
@Default(CameraPosition.front) CameraPosition currentPosition,
@Default(CameraPosition.back) CameraPosition currentPosition,
@Default(false) bool isLoading,
@Default(false) bool isPop,
@Default(false) bool isAudioEnable,
}) = _StreamCameraViewState;
}
Expand Down
Loading

0 comments on commit 936394c

Please sign in to comment.