I'm working on attempting to add Android implementation to https://github.com/jfversluis/Plugin.Maui.ScreenRecording
I think I checked all bases, but I'm getting this exception on MediaRecorder.Stop();
{Java.Lang.RuntimeException: stop failed.
at Java.Interop.JniEnvironment.InstanceMethods.CallVoidMethod(JniObjectReference instance, JniMethodInfo method, JniArgumentValue* args) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/obj/Release/net7.0/JniEnvironment.g.cs:line 20370
at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualVoidMethod(String encodedMember, IJavaPeerable self, JniArgumentValue* parameters) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods_Invoke.cs:line 66
at Android.Media.MediaRecorder.Stop() in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-34/mcw/Android.Media.MediaRecorder.cs:line 2477
at Plugin.Maui.ScreenRecording.ScreenRecordingImplementation.StopRecording(ScreenRecordingOptions options) in D:\pmsr\src\Plugin.Maui.ScreenRecording\ScreenRecording.android.cs:line 84
--- End of managed Java.Lang.RuntimeException stack trace ---
java.lang.RuntimeException: stop failed.
at android.media.MediaRecorder.stop(Native Method)
at crc64fcf28c0e24b4cc31.ButtonHandler_ButtonClickListener.n_onClick(Native Method)
at crc64fcf28c0e24b4cc31.ButtonHandler_ButtonClickListener.onClick(ButtonHandler_ButtonClickListener.java:31)
at android.view.View.performClick(View.java:7448)
at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1211)
at android.view.View.performClickInternal(View.java:7425)
at android.view.View.access$3600(View.java:810)
at android.view.View$PerformClick.run(View.java:28305)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
--- End of managed Java.Lang.RuntimeException stack trace ---
java.lang.RuntimeException: stop failed.
at android.media.MediaRecorder.stop(Native Method)
at crc64fcf28c0e24b4cc31.ButtonHandler_ButtonClickListener.n_onClick(Native Method)
at crc64fcf28c0e24b4cc31.ButtonHandler_ButtonClickListener.onClick(ButtonHandler_ButtonClickListener.java:31)
at android.view.View.performClick(View.java:7448)
at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1211)
at android.view.View.performClickInternal(View.java:7425)
at android.view.View.access$3600(View.java:810)
at android.view.View$PerformClick.run(View.java:28305)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
}
I'm guessing it is something stupid I am looking over. Any guidance or suggestions would be greatly appreciated.
ScreenRecording.android.cs
using Android.Content;
using Android.Media;
using Android.Media.Projection;
using Android.Hardware.Display;
using Android.Views;
using Microsoft.Maui.ApplicationModel;
using Android.Content.PM;
using Android.OS;
using AndroidX.Core.App;
using Android.Graphics.Drawables;
using AndroidX.Core.Graphics.Drawable;
using Microsoft.Maui.Devices;
using static Android.Provider.Telephony.Mms;
using Microsoft.Maui.Storage;
namespace Plugin.Maui.ScreenRecording;
public partial class ScreenRecordingImplementation : IScreenRecording
{
public MediaProjectionManager ProjectionManager;
public MediaProjection MediaProjection;
public VirtualDisplay VirtualDisplay;
public MediaRecorder MediaRecorder;
public string FilePath;
public const int REQUEST_MEDIA_PROJECTION = 1;
public static EventHandler<ScreenRecordingEventArgs> ScreenRecordingPermissionHandler;
public ScreenRecordingImplementation()
{
ProjectionManager = (MediaProjectionManager)Platform.AppContext.GetSystemService(Context.MediaProjectionService);
}
public bool IsRecording { get; private set; }
public bool IsSupported { get { return ProjectionManager != null; } private set { }}
public async Task StartRecording(bool enableMicrophone)
{
if (IsSupported)
{
MediaRecorder = new MediaRecorder();
//FilePath = Path.Combine(Platform.AppContext.FilesDir.AbsolutePath, $"recording_{DateTime.Now:yyyyMMdd_HHmmss}.mp4");
FilePath = Path.Combine(FileSystem.Current.CacheDirectory, "Screen.mp4");
SetUpMediaRecorder(enableMicrophone);
try
{
await Task.Delay(2000);
MediaRecorder.Start();
IsRecording = true;
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
throw new NotSupportedException("Screen recording did not start.");
}
}
else
{
throw new NotSupportedException("Screen recording not supported on this device.");
}
}
public async Task<ScreenRecordingFile?> StopRecording(ScreenRecordingOptions? options)
{
if (IsRecording)
{
IsRecording = false;
try
{
MediaRecorder.Stop();
MediaRecorder.Release();
VirtualDisplay.Release();
MediaProjection.Stop();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
var context = Android.App.Application.Context;
context.StopService(new Intent(context, typeof(ScreenRecordingService)));
return new ScreenRecordingFile(FilePath);
}
return null;
}
public async void OnScreenCapturePermissionGranted(Result resultCode, Intent? data)
{
if (resultCode == Result.Ok && data != null)
{
var context = Android.App.Application.Context;
Intent serviceIntent = new Intent(context, typeof(ScreenRecordingService));
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
context.StartForegroundService(serviceIntent);
}
else
{
context.StartService(serviceIntent);
}
await Task.Delay(2000);
// Handle the successful permission result
MediaProjection mediaProjection = ProjectionManager.GetMediaProjection((int)resultCode, data);
VirtualDisplay = mediaProjection.CreateVirtualDisplay("Plugin.Maui.ScreenRecording.ScreenRecordingImplementation.ScreenRecordingService", (int)DeviceDisplay.Current.MainDisplayInfo.Width, (int)DeviceDisplay.Current.MainDisplayInfo.Height, (int)DeviceDisplay.Current.MainDisplayInfo.Density, Android.Views.DisplayFlags.Presentation, null, null, null);
}
else
{
// Handle the case where permission was not granted
}
// Additional setup or start recording
}
public void SetUpMediaRecorder(bool enableMicrophone)
{
MediaRecorder = new MediaRecorder();
if (enableMicrophone)
{
MediaRecorder.SetAudioSource(AudioSource.Mic);
}
MediaRecorder.SetVideoSource(VideoSource.Surface);
MediaRecorder.SetOutputFormat(OutputFormat.Mpeg4);
MediaRecorder.SetVideoEncoder(VideoEncoder.H264);
if (enableMicrophone)
{
MediaRecorder.SetAudioEncoder(AudioEncoder.AmrNb);
}
MediaRecorder.SetOutputFile(FilePath);
MediaRecorder.SetVideoSize((int)DeviceDisplay.Current.MainDisplayInfo.Width, (int)DeviceDisplay.Current.MainDisplayInfo.Height);
MediaRecorder.SetVideoFrameRate(30);
try
{
MediaRecorder.Prepare();
}
catch (Java.IO.IOException ex)
{
Console.WriteLine($"MediaRecorder preparation failed: {ex.Message}");
// Handle preparation failure
MediaRecorder.Release();
}
}
public void Setup()
{
Intent captureIntent = ProjectionManager.CreateScreenCaptureIntent();
Platform.CurrentActivity.StartActivityForResult(captureIntent, REQUEST_MEDIA_PROJECTION);
}
public class ScreenRecordingEventArgs : EventArgs
{
public Result ResultCode { get; set; }
public Intent? Data { get; set; }
}
[Service(ForegroundServiceType = ForegroundService.TypeMediaProjection)]
public class ScreenRecordingService : Service
{
public override IBinder? OnBind(Intent? intent)
{
return null;
}
public override StartCommandResult OnStartCommand(Intent? intent, StartCommandFlags flags, int startId)
{
string CHANNEL_ID = "ScreenRecordingService";
int NOTIFICATION_ID = 1337;
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
var channelName = "Screen Recording Service";
var channel = new NotificationChannel(CHANNEL_ID, channelName, NotificationImportance.Default)
{
Description = "Notification Channel for Screen Recording Service"
};
var notificationManager = (NotificationManager)GetSystemService(NotificationService);
notificationManager.CreateNotificationChannel(channel);
}
// Create a notification for the foreground service
var notificationBuilder = new Notification.Builder(this, CHANNEL_ID)
.SetContentTitle("Screen Recording")
.SetContentText("Recording screen...")
.SetSmallIcon(Resource.Drawable.notification_template_icon_low_bg); // Ensure you have 'ic_notification' in Resources/drawable
var notification = notificationBuilder.Build();
// Start the service in the foreground
StartForeground(NOTIFICATION_ID, notification);
return StartCommandResult.Sticky;
}
}
}
MainActivity.cs
using Android.App;
using Android.Content.PM;
using Android.OS;
using Plugin.Maui.ScreenRecording;
namespace ScreenRecordingSample;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
public const int REQUEST_MEDIA_PROJECTION = 1;
ScreenRecordingImplementation screenRecordingImplementation = new ScreenRecordingImplementation();
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
// Initialize other components...
// Initialize ScreenRecordingImplementation
screenRecordingImplementation = new Plugin.Maui.ScreenRecording.ScreenRecordingImplementation();
}
protected override void OnActivityResult(int requestCode, Result resultCode, Android.Content.Intent? data)
{
base.OnActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_MEDIA_PROJECTION)
{
screenRecordingImplementation.OnScreenCapturePermissionGranted(resultCode, data);
}
}
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:supportsRtl="true">
<service android:name="Plugin.Maui.ScreenRecording.ScreenRecordingImplementation.ScreenRecordingService" android:foregroundServiceType="mediaProjection" />
</application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-sdk />
</manifest>
IScreenRecording.cs
namespace Plugin.Maui.ScreenRecording;
/// <summary>
/// Provides the ability to record the screen within your app.
/// </summary>
public interface IScreenRecording
{
/// <summary>
/// Gets whether or not screen recording is currently happening.
/// </summary>
bool IsRecording { get; }
/// <summary>
/// Gets whether or not screen recording is supported on this device.
/// </summary>
bool IsSupported { get; }
/// <summary>
/// Sets up the screenrecording.
/// </summary>
/// <remarks>
/// This is needed for android to request media projection.
/// </remarks>
void Setup();
/// <summary>
/// Starts the screenrecording.
/// </summary>
/// <param name="enableMicrophone">Determines if the microphone should be used as input during the recording.</param>
/// <returns>A <see cref="Task"/> object with information about this operation.</returns>
/// <remarks>
/// A permission to access the microphone might be needed, this is not requested by this method.
/// Make sure to add an entry in the metadata of your app and request the runtime permission
/// before calling this method.
/// </remarks>
Task StartRecording(bool enableMicrophone);
/// <summary>
/// Stops the recording and saves the video file to the device's gallery.
/// </summary>
/// <returns>A <see cref="Task"/> object with information about this operation.</returns>
Task<ScreenRecordingFile?> StopRecording(ScreenRecordingOptions? options = null);
}
The rest of the code is the same as the main repository I believe.
First, surround the
MediaRecorder.Stop()
call with a try-catch block to handle any runtime exceptions gracefully. That will not solve the root cause but can prevent the app from crashing.The
MediaRecorder.Stop()
method will fail if the recording has not actually started or if it is already stopped. Make sureMediaRecorder.Start()
is successfully called before invokingStop()
.The
MediaRecorder
class is also very sensitive to the order of method calls. Make sure that the sequence ofPrepare()
,Start()
, andStop()
calls is correct. You must callPrepare()
beforeStart()
, andStart()
must have enough time to initiate the recording before you callStop()
.If the recording is stopped immediately after it starts, the
MediaRecorder
might not have enough data to finalize the video file, leading to this exception. Introduce a delay between start and stop to make sure some data is recorded.After stopping the recording, make sure resources are released properly. Call
MediaRecorder.Release()
afterMediaRecorder.Stop()
. Also, release theVirtualDisplay
andMediaProjection
objects.True, you should use the DI container to resolve your
IScreenRecording
instance.But, as you have discovered, injecting dependencies directly into the
MainActivity
constructor is not straightforward because Android requires a default constructor for activities.A possible workaround would, instead of using constructor injection, to use property or method injection mentioned in "Dependency injection". That can be done after the activity is created and its dependencies are ready to be injected.
The article "Are you using Dependency Injection in your .NET MAUI app yet?" from Julian Ewers-Peters mentions "service locator pattern" to resolve dependencies (examples in "Page Resolver and Navigation Extension for MAUI" from Matt Goldman), but cautions
But a commonly used workaround would be to resolve the dependency in the
OnCreate
method (see ".NET MAUI / App lifecycle") of yourMainActivity
. That makes sure the dependency is resolved as soon as the activity is created, but before it is used.For that last approach, your
MainActivity
using DI forIScreenRecording
would be: