Skip to content

Commit

Permalink
Merge 2df34a3 into 545712c
Browse files Browse the repository at this point in the history
  • Loading branch information
romtsn authored Mar 25, 2024
2 parents 545712c + 2df34a3 commit 8569361
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,7 @@ internal class ReplayCache(
private val encoderCreator: (File) -> SimpleVideoEncoder = { videoFile ->
SimpleVideoEncoder(
options,
MuxerConfig(
file = videoFile,
recorderConfig = recorderConfig,
frameRate = recorderConfig.frameRate.toFloat(),
bitrate = 20 * 1000
)
MuxerConfig(file = videoFile, recorderConfig = recorderConfig)
).also { it.start() }
}
) : Closeable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@ package io.sentry.android.replay

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.Rect
import android.os.Build
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.view.WindowManager
import io.sentry.DateUtils
import io.sentry.Hint
import io.sentry.IHub
Expand All @@ -33,7 +30,6 @@ import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import kotlin.LazyThreadSafetyMode.NONE
import kotlin.math.roundToInt

class ReplayIntegration(
private val context: Context,
Expand All @@ -59,28 +55,11 @@ class ReplayIntegration(
private val saver =
Executors.newSingleThreadScheduledExecutor(ReplayExecutorServiceThreadFactory())

private val screenBounds by lazy(NONE) {
// PixelCopy takes screenshots including system bars, so we have to get the real size here
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
if (VERSION.SDK_INT >= VERSION_CODES.R) {
wm.currentWindowMetrics.bounds
} else {
val screenBounds = Point()
@Suppress("DEPRECATION")
wm.defaultDisplay.getRealSize(screenBounds)
Rect(0, 0, screenBounds.x, screenBounds.y)
}
}

private val aspectRatio by lazy(NONE) {
screenBounds.bottom.toFloat() / screenBounds.right.toFloat()
}

private val recorderConfig by lazy(NONE) {
ScreenshotRecorderConfig(
recordingWidth = (720 / aspectRatio).roundToInt(),
recordingHeight = 720,
scaleFactor = 720f / screenBounds.bottom
ScreenshotRecorderConfig.from(
context,
targetHeight = 720,
options._experimental.sessionReplayOptions
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
package io.sentry.android.replay

import android.annotation.TargetApi
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Bitmap.Config.ARGB_8888
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Point
import android.graphics.Rect
import android.graphics.RectF
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.view.PixelCopy
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.WindowManager
import io.sentry.SentryLevel.DEBUG
import io.sentry.SentryLevel.INFO
import io.sentry.SentryOptions
import io.sentry.SessionReplayOptions
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode
import java.lang.ref.WeakReference
import java.util.WeakHashMap
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.roundToInt
import kotlin.system.measureTimeMillis

@TargetApi(26)
Expand Down Expand Up @@ -217,8 +224,33 @@ internal data class ScreenshotRecorderConfig(
val recordingWidth: Int,
val recordingHeight: Int,
val scaleFactor: Float,
val frameRate: Int = 2
)
val frameRate: Int,
val bitRate: Int
) {
companion object {
fun from(context: Context, targetHeight: Int, sessionReplayOptions: SessionReplayOptions): ScreenshotRecorderConfig {
// PixelCopy takes screenshots including system bars, so we have to get the real size here
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val screenBounds = if (VERSION.SDK_INT >= VERSION_CODES.R) {
wm.currentWindowMetrics.bounds
} else {
val screenBounds = Point()
@Suppress("DEPRECATION")
wm.defaultDisplay.getRealSize(screenBounds)
Rect(0, 0, screenBounds.x, screenBounds.y)
}
val aspectRatio = screenBounds.bottom.toFloat() / screenBounds.right.toFloat()

return ScreenshotRecorderConfig(
recordingWidth = (targetHeight / aspectRatio).roundToInt(),
recordingHeight = targetHeight,
scaleFactor = targetHeight.toFloat() / screenBounds.bottom,
frameRate = sessionReplayOptions.frameRate,
bitRate = sessionReplayOptions.bitRate
)
}
}
}

interface ScreenshotRecorderCallback {
fun onScreenshotRecorded(bitmap: Bitmap)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ internal class SimpleVideoEncoder(
MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
)
format.setInteger(MediaFormat.KEY_BIT_RATE, muxerConfig.bitrate)
format.setFloat(MediaFormat.KEY_FRAME_RATE, muxerConfig.frameRate)
format.setInteger(MediaFormat.KEY_BIT_RATE, muxerConfig.recorderConfig.bitRate)
format.setFloat(MediaFormat.KEY_FRAME_RATE, muxerConfig.recorderConfig.frameRate.toFloat())
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10)

format
Expand All @@ -79,7 +79,7 @@ internal class SimpleVideoEncoder(
}

private val bufferInfo: MediaCodec.BufferInfo = MediaCodec.BufferInfo()
private val frameMuxer = muxerConfig.frameMuxer
private val frameMuxer = SimpleMp4FrameMuxer(muxerConfig.file.absolutePath, muxerConfig.recorderConfig.frameRate.toFloat())
val duration get() = frameMuxer.getVideoTime()

private var surface: Surface? = null
Expand Down Expand Up @@ -187,8 +187,5 @@ internal class SimpleVideoEncoder(
internal data class MuxerConfig(
val file: File,
val recorderConfig: ScreenshotRecorderConfig,
val bitrate: Int = 20_000,
val frameRate: Float = recorderConfig.frameRate.toFloat(),
val mimeType: String = MediaFormat.MIMETYPE_VIDEO_AVC,
val frameMuxer: SimpleFrameMuxer = SimpleMp4FrameMuxer(file.absolutePath, frameRate)
val mimeType: String = MediaFormat.MIMETYPE_VIDEO_AVC
)
20 changes: 20 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,12 @@ public abstract interface class io/sentry/EventProcessor {
public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction;
}

public final class io/sentry/ExperimentalOptions {
public fun <init> ()V
public fun getSessionReplayOptions ()Lio/sentry/SessionReplayOptions;
public fun setSessionReplayOptions (Lio/sentry/SessionReplayOptions;)V
}

public final class io/sentry/ExternalOptions {
public fun <init> ()V
public fun addBundleId (Ljava/lang/String;)V
Expand Down Expand Up @@ -2315,6 +2321,7 @@ public class io/sentry/SentryOptions {
public fun getTransportFactory ()Lio/sentry/ITransportFactory;
public fun getTransportGate ()Lio/sentry/transport/ITransportGate;
public final fun getViewHierarchyExporters ()Ljava/util/List;
public fun get_experimental ()Lio/sentry/ExperimentalOptions;
public fun isAttachServerName ()Z
public fun isAttachStacktrace ()Z
public fun isAttachThreads ()Z
Expand Down Expand Up @@ -2686,6 +2693,19 @@ public final class io/sentry/Session$State : java/lang/Enum {
public static fun values ()[Lio/sentry/Session$State;
}

public final class io/sentry/SessionReplayOptions {
public fun <init> ()V
public fun <init> (Ljava/lang/Double;Ljava/lang/Double;)V
public fun getBitRate ()I
public fun getErrorReplayDuration ()J
public fun getErrorSampleRate ()Ljava/lang/Double;
public fun getFrameRate ()I
public fun getSessionSampleRate ()Ljava/lang/Double;
public fun getSessionSegmentDuration ()J
public fun setErrorSampleRate (Ljava/lang/Double;)V
public fun setSessionSampleRate (Ljava/lang/Double;)V
}

public final class io/sentry/ShutdownHookIntegration : io/sentry/Integration, java/io/Closeable {
public fun <init> ()V
public fun <init> (Ljava/lang/Runtime;)V
Expand Down
16 changes: 16 additions & 0 deletions sentry/src/main/java/io/sentry/ExperimentalOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.sentry;

import org.jetbrains.annotations.NotNull;

public final class ExperimentalOptions {
private @NotNull SessionReplayOptions sessionReplayOptions = new SessionReplayOptions();

@NotNull
public SessionReplayOptions getSessionReplayOptions() {
return sessionReplayOptions;
}

public void setSessionReplayOptions(final @NotNull SessionReplayOptions sessionReplayOptions) {
this.sessionReplayOptions = sessionReplayOptions;
}
}
7 changes: 7 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,8 @@ public class SentryOptions {
*/
private int profilingTracesHz = 101;

private final @NotNull ExperimentalOptions _experimental = new ExperimentalOptions();

/**
* Adds an event processor
*
Expand Down Expand Up @@ -2274,6 +2276,11 @@ public void setSessionFlushTimeoutMillis(final long sessionFlushTimeoutMillis) {
this.sessionFlushTimeoutMillis = sessionFlushTimeoutMillis;
}

@NotNull
public ExperimentalOptions get_experimental() {
return _experimental;
}

/** The BeforeSend callback */
public interface BeforeSendCallback {

Expand Down
85 changes: 85 additions & 0 deletions sentry/src/main/java/io/sentry/SessionReplayOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package io.sentry;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;

public final class SessionReplayOptions {

/**
* Indicates the percentage in which the replay for the session will be created. Specifying 0
* means never, 1.0 means always. The value needs to be >= 0.0 and <= 1.0 The default is null
* (disabled).
*/
private @Nullable Double sessionSampleRate;

/**
* Indicates the percentage in which a 30 seconds replay will be send with error events.
* Specifying 0 means never, 1.0 means always. The value needs to be >= 0.0 and <= 1.0. The
* default is null (disabled).
*/
private @Nullable Double errorSampleRate;

/**
* Defines the quality of the session replay. Higher bit rates have better replay quality, but
* also affect the final payload size to transfer. The default value is 20kbps;
*/
private int bitRate = 20_000;

/**
* Number of frames per second of the replay. The bigger the number, the more accurate the replay
* will be, but also more data to transfer and more CPU load.
*/
private int frameRate = 1;

/** The maximum duration of replays for error events. */
private long errorReplayDuration = 30_000L;

/** The maximum duration of the segment of a session replay. */
private long sessionSegmentDuration = 5000L;

public SessionReplayOptions() {}

public SessionReplayOptions(
final @Nullable Double sessionSampleRate, final @Nullable Double errorSampleRate) {
this.sessionSampleRate = sessionSampleRate;
this.errorSampleRate = errorSampleRate;
}

@Nullable
public Double getErrorSampleRate() {
return errorSampleRate;
}

public void setErrorSampleRate(final @Nullable Double errorSampleRate) {
this.errorSampleRate = errorSampleRate;
}

@Nullable
public Double getSessionSampleRate() {
return sessionSampleRate;
}

public void setSessionSampleRate(final @Nullable Double sessionSampleRate) {
this.sessionSampleRate = sessionSampleRate;
}

@ApiStatus.Internal
public int getBitRate() {
return bitRate;
}

@ApiStatus.Internal
public int getFrameRate() {
return frameRate;
}

@ApiStatus.Internal
public long getErrorReplayDuration() {
return errorReplayDuration;
}

@ApiStatus.Internal
public long getSessionSegmentDuration() {
return sessionSegmentDuration;
}
}

0 comments on commit 8569361

Please sign in to comment.