Coder Social home page Coder Social logo

appstream-session-recording's Introduction

How to record a video of Amazon AppStream 2.0 streaming sessions

The solution lets you record a video of AppStream 2.0 streaming sessions by using FFmpeg, a popular media framework. For more information, see the AWS Security Blog post How to record a video of Amazon AppStream 2.0 streaming sessions.

Overview and architecture

AppStream 2.0 enables you to run custom scripts to prepare the streaming instance before the applications launch or after the streaming session has completed. Figure 1 shows a simplified description of what happens before, during and after a streaming session.

Solution architecture diagram

  1. Before the streaming session starts, AppStream 2.0 runs script A, which uses PsExec, a utility that enables administrators to run commands on local or remote computers, to launch script B. Script B then runs during the entire streaming session. PsExec can run the script as the LocalSystem account, a service account that has extensive privileges on a local system, while it interacts with the desktop of another session. Using the LocalSystem account, you can use FFmpeg to record the session screen and prevent AppStream 2.0 users from stopping or tampering with the solution, as long as they aren’t granted local administrator rights.

  2. Script B launches FFmpeg and starts recording the desktop. The solution uses the FFmpeg built-in screen-grabber to capture the desktop across all the available screens.

  3. When FFmpeg starts recording, it captures the area covered by the desktop at that time. If the number of screens or the resolution changes, a portion of the desktop might be outside the recorded area. In that case, script B stops the recording and starts FFmpeg again.

  4. After the streaming session ends, AppStream 2.0 runs script C, which notifies script B that it must end the recording and close. Script B stops FFmpeg.

  5. Before exiting, script B uploads the video files that FFmpeg generated to Amazon Simple Storage Service (Amazon S3). It also stores user and session metadata in Amazon S3, along with the video files, for easy retrieval of session recordings.

For a more comprehensive understanding of how the session scripts works, you can refer to the next section, where I go into the details of each script.

The solution scripts

In this section, I go into the details of each of the PowerShell scripts that compose the solution.

Before the streaming session begins

The solution runs the following script within the system context before streaming sessions start. We wait until the user session is active, and we save the session ID and user name for later use.

while ($True) {
  $SessionActive = $False
  $Sessions = @(query session) -split '\n' | Select-Object -Skip 1
    
  foreach($Session in $Sessions) {
    $ParsedSession = $Session -split '\s{2,}'
    $SessionUserName = $ParsedSession[1]
    $SessionId = $ParsedSession[2]
    $SessionState = $ParsedSession[3]
    if ($SessionState -eq 'Active') {
      $SessionActive = $True
      Break
    }
  }

  if ($SessionActive) {
    Break
  }
  else {
    Start-Sleep -Seconds 1
  }
}

The script launches a second PowerShell script, shown following, with PsExec. We use PsExec because it can run a program as the LocalSystem account so that the program interacts with the desktop of another session. By doing so, we can record the session screen, while preventing AppStream 2.0 users from stopping or tampering with the solution, as long as they aren’t granted local administrator rights.

C:\SessionRecording\Bin\PsExec64.exe -d -i $SessionId -s -accepteula C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe -NonInteractive -WindowStyle Hidden -File C:\SessionRecording\Scripts\main.ps1 -UserName $SessionUserName

At this stage, the streaming session starts, and the script that was launched with PsExec runs during the entire session lifetime.

During the streaming session

AppStream 2.0 provides metadata about users, sessions, and instances through Windows environment variables. For the solution in this blog, we store user and session metadata in Amazon S3 for easy retrieval of session recordings. User and session metadata is only available within the user context, but environment variables that exist within another context can be obtained from the registry. We retrieve the session user’s Security ID (SID) and we fetch the environment variables from the registry.

param ($UserName)
$Domain = ((gcim Win32_LoggedOnUser).Antecedent | Where-Object {$_.Name -eq $UserName} | Select-Object Domain -Unique).Domain

$User = New-Object System.Security.Principal.NTAccount($Domain, $UserName)
$Sid = $User.Translate([System.Security.Principal.SecurityIdentifier]).Value

$UserEnvVar = [ordered]@{}
New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS | Out-Null
$RegKey = (Get-ItemProperty "HKU:\${sid}\Environment")
$RegKey.PSObject.Properties | ForEach-Object {
  $UserEnvVar.Add($_.Name, $_.Value)
}
Remove-PSDrive -Name HKU

The script writes the metadata to a text file and uploads it to Amazon S3. We include the stack name, the fleet name, and the session ID in the S3 prefix, so that you can easily find all recordings for a given stack, fleet, or session.

$Metadata = @{
  StackName = $UserEnvVar.AppStream_Stack_Name;
  UserAccessMode = $UserEnvVar.AppStream_User_Access_Mode;
  SessionReservationDateTime = $UserEnvVar.AppStream_Session_Reservation_DateTime;
  UserName = $UserEnvVar.AppStream_UserName;
  SessionId = $UserEnvVar.AppStream_Session_ID;
  ImageArn = (Get-Item Env:AppStream_Image_Arn).Value;
  InstanceType = (Get-Item Env:AppStream_Instance_Type).Value;
  FleetName = (Get-Item Env:AppStream_Resource_Name).Value
}

$Content = $Metadata | ConvertTo-Json
Set-Content -Path C:\SessionRecording\Output\metadata.txt -Value $Content

$Date = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
$Key = "$($BUCKET_PREFIX)$($Metadata.StackName)/$($Metadata.FleetName)/$($Metadata.SessionId)/$($Date)-metadata.txt"
Write-S3Object -BucketName $BUCKET_NAME -Key $Key -File C:\SessionRecording\Output\metadata.txt -Region $BUCKET_REGION -ProfileName appstream_machine_role

The script then repeats the following set of commands every second, in an infinite loop.

$FfmpegProcess = $null
$CurrentResolution = $null

while ($True) {

  if (!($FfmpegProcess) -or ($FfmpegProcess -and $FfmpegProcess.HasExited)) {
    StartRecording
  }
  
  $NewResolution = (Get-DisplayResolution) -replace $([char]0) | Where-Object { $_ -ne "" }
  if (ResolutionHasChanged -NewResolution $NewResolution) {
    StopRecording
  }
  $CurrentResolution = $NewResolution
  
  UploadVideoFileToS3
    
   if (SessionIsClosing) {
    StopRecording
    WaitUntilAllVideosAreUploaded
    WriteEndMarker
    Break
  }

  Start-Sleep -Seconds 1
}

During each loop iteration, these actions happen:

  • The script launches FFmpeg if no FFmpeg process exists, or if it has exited. We configure FFmpeg to capture FRAME_RATE frames per second, and to produce one video file every VIDEO_MAX_DURATION seconds whose name contains the time when the recording started. The default value are, respectively, 5 frames per second and 300 seconds. You can adapt these values to your own needs. We redirect the input such that the script can simulate a “q” key press when we need to close FFmpeg.
function StartRecording {
  $Date = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
  $Arguments = "-f gdigrab -framerate $($FRAME_RATE) -t $($VIDEO_MAX_DURATION) -y -v 0 -i desktop -vcodec libx264 -pix_fmt yuv420p C:\SessionRecording\Output\$($Date)-video.mp4"

  $pinfo = New-Object System.Diagnostics.ProcessStartInfo
  $pinfo.FileName = "C:\SessionRecording\Bin\ffmpeg.exe"
  $pinfo.Arguments = $Arguments
  $pinfo.WindowStyle = "Hidden"
  $pinfo.UseShellExecute = $false
  $pinfo.WindowStyle = "Hidden"
  $pinfo.RedirectStandardInput = $true

  $p = New-Object System.Diagnostics.Process
  $p.StartInfo = $pinfo
  $p.Start()
  $p.PriorityClass = "REALTIME"
  $script:FfmpegProcess =  $p
}
  • We check whether the number of screens or the screen resolution changed. When FFmpeg starts recording, it captures the area covered by the desktop at that time. If the number of screens or the resolution changes, a portion of the desktop might be outside the recorded region. In that case, we stop FFmpeg by simulating a “q” key press. FFmpeg will be restarted during the next loop iteration.
function ResolutionHasChanged {
  param($NewResolution)

  if ($script:CurrentResolution -eq $null) {
    return $False
  }

  # Put the variable into an array if the variable has a single line (one screen)
  if (($script:CurrentResolution | Measure-Object -Line).Lines -eq 1) {
    $Current = @($script:CurrentResolution)
  }
  else {
    $Current = $script:CurrentResolution.split('\n')
  }

  if (($NewResolution | Measure-Object -Line).Lines -eq 1) {
    $New = @($NewResolution)
  }
  else {
    $New = $NewResolution.split('\n')
  }

  # If the number of screens differs, return True. Otherwise, check if each screen resolution differs
  if ($Current.Count -ne $New.Count) {
    return $True
  }
  else {
    for ($i = 0; $i -lt $Current.Count; $i++) {
      if ($Current[$i] -ne $New[$i]) {
        return $True
      }
    }
  }

  return $False
}

function StopRecording {
  if ($script:FfmpegProcess -ne $null) {
    $script:FfmpegProcess.StandardInput.Write('q')
  }
}
  • The script uploads the video files that exist in the local disk, except the video file that is being written by the current FFmpeg process. Once the upload succeeds, we remove the video files from the local disk.
function UploadVideoFileToS3 {
  $AllVideosUploaded = $True

  foreach ($Video in (Get-Item -Path C:\SessionRecording\Output\*.mp4)) {

    # I check if the video is being generated by a running FFmpeg process
    $CurrentVideo = $False
    foreach ($Process in (Get-WmiObject Win32_Process -Filter "name = 'ffmpeg.exe'" | Select-Object CommandLine)) {
      if ($Process.CommandLine -like "*$($Video.Name)") {
        $CurrentVideo = $True
        $AllVideosUploaded = $False
      }
    }

    # I upload and delete the video if it is not being generated by FFmpeg
    if ($CurrentVideo -eq $True) {
      Continue
    }
    try {
      $Key = "$($BUCKET_PREFIX)$($Metadata.StackName)/$($Metadata.FleetName)/$($Metadata.SessionId)/$($Video.Name)"
      Write-S3Object -BucketName $BUCKET_NAME -Key $Key -File "C:\SessionRecording\Output\$($Video.Name)" -Region $BUCKET_REGION -ProfileName appstream_machine_role -ErrorAction Stop
      Remove-Item "C:\SessionRecording\Output\$($Video.Name)" -ErrorAction Stop
    }
    catch {
      $AllVideosUploaded = $False
      Continue
    }
  }

  # I return True if there are no more pending videos to upload
  return $AllVideosUploaded
}
  • The last command in the loop is discussed in the next section.

After the streaming session ends

AppStream 2.0 runs a third script within the system context after the streaming sessions ends. This script writes a file named ended.txt to the local disk, which is used to notify the second script that the session ended. Then, the third script waits until the second script deletes that file.

Set-Content -Path C:\SessionRecording\Scripts\ended.txt -Value "ended"

while ((Test-Path -Path C:\SessionRecording\Scripts\ended.txt)) {
  Start-Sleep -Seconds 1
}

The last command in the loop of the second script checks whether this file exists. If it exists, the second script stops FFmpeg, uploads the video files to Amazon S3 until all pending video files are successfully uploaded, and exits the loop.

function SessionIsClosing {
   return (Test-Path C:\SessionRecording\Scripts\ended.txt)
}

function WaitUntilAllVideosAreUploaded {
  # I retry 5 times to upload the remaining videos
  for ($i=0; $i -lt 5; $i++) {
    if ((UploadVideoFileToS3) -eq $True) {
      Break
    }
    else {
      Start-Sleep -Seconds 1
    }
  }
}

After the loop exited, the second script deletes the file ended.txt, and both the second and third scripts terminate.

while ($True) {
  try {
    Remove-Item C:\SessionRecording\Scripts\ended.txt -ErrorAction Stop
    Break
  }
  catch {
    # I retry if the file failed to delete because it is locked
    Start-Sleep -Seconds 1
    Continue
  }
}

Security

See CONTRIBUTING for more information.

License

This library is licensed under the MIT-0 License. See the LICENSE file.

appstream-session-recording's People

Contributors

amazon-auto avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.