GithubHelp home page GithubHelp logo

lausbert / snaptake Goto Github PK

View Code? Open in Web Editor NEW
13.0 3.0 1.0 515 KB

Using Fastlane Snapshot to automate creating of appstore preview videos

Home Page: https://twitter.com/Lausbert3000

Swift 41.41% Ruby 46.50% HTML 12.09%
video fastlane record-video edit-videos fastlane-snapshot ios xcode uitesting automation appstore

snaptake's Introduction

Check out my blog post!

Snaptake

There are already some tries (here and here) to automate creating videos with fastlane snapshot. Anyway Felix definitely does not want to have an http server in his code base :)

The following step by step guide shows an alternative way of solution. The outline corresponds to the commit history.

Prerequisites

Creating project

Just create a single view application in XCode without any tests.

Setting up fastlane snapshot

cd to your project folder and run fastlane init. When fastlane is asking what you would like to use it for press "1".

Do exactly what fastlane is telling you afterwards. After finishing the instructions open the newly created Snapfile in your projects fastlane folder. Uncomment at least one device and one language. Make sure snapshot("0Launch") is called in one of your UITests. Run fastlane snapshot in your project folder to verify everything is working fine.

Setting up storyboard

To test the later added video recording feature, we need something to record. Therefore just add a button to your first ViewController and add a second ViewController with a distinguishable background. Push the second ViewController, when the button is clicked. Also set the buttons accessibility identifier to "button".

Setting up UITests

Now that your storyboard is set up, let's add video related code to the SnapshotHelper file. To keep the original snapshot logic, add the following two functions to the SnapshotHelper file scope.

func snaptake(_ name: String, waitForLoadingIndicator: Bool, plot: ()->()) {
    if waitForLoadingIndicator {
        Snapshot.snaptake(name, plot: plot)
    } else {
        Snapshot.snaptake(name, timeWaitingForIdle: 0, plot: plot)
    }
}
/// - Parameters:
///   - name: The name of the snaptake
///   - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait.
///   - plot: Plot which should be recorded.
func snaptake(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20, plot: ()->()) {
    Snapshot.snaptake(name, timeWaitingForIdle: timeout, plot: plot)
}

These two functions are pretty similar to the already existing snapshot functions. The only difference lies in the additional argument plot: ()->(), which is a closure with no parameters and return values. plot contains all the interface interactions you want to record. You will see how to use it later.

Within your Snapshot class add the actual recording logic. snaptake takes plot as an argument and successively calls snaptakeStart(), snaptakeSetTrimmingFlag(), plot() and snaptakeStop().

open class func snaptake(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20, plot: ()->()) {
        
    guard let recordingFlagPath = snaptakeStart(name, timeWaitingForIdle: timeout) else { return }

    snaptakeSetTrimmingFlag()

    plot()

    snaptakeStop(recordingFlagPath)
}

Within snaptakeStart a recordingFlag is saved to your hard drive. This recordingFlag contains the path of the later recorded video. The saving of this recordingFlag is watched outside of XCode to start the actual recording process. You will see how this works later.

class func snaptakeStart(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) -> URL? {
    if timeout > 0 {
        waitForLoadingIndicatorToDisappear(within: timeout)
    }

    print("snaptake: \(name)")

    sleep(1) // Waiting for the animation to be finished (kind of)

    #if os(OSX)
    XCUIApplication().typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: [])
    #else
    guard let simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return nil }

    let path = "screenshots/\(locale)/\(simulator)-\(name).mp4"
    let recordingFlagPath = screenshotsDir.appendingPathComponent("recordingFlag.txt")

    do {
        try path.write(to: recordingFlagPath, atomically: false, encoding: String.Encoding.utf8)
    } catch let error {
        print("Problem setting recording flag: \(recordingFlagPath)")
        print(error)
    }
    #endif
    return recordingFlagPath
}

There is a pretty annoying bug, when recording videos via console: The first few frames appear black until somethings happens within your application. That's why we are going to rotate the device and save related duration in snaptakeSetTrimmingFlag. Later we will trim the recorded video accordingly.

class func snaptakeSetTrimmingFlag() {

    let start = Date()
    sleep(2)
    XCUIDevice.shared.orientation = .landscapeLeft
    sleep(2)
    XCUIDevice.shared.orientation = .portrait
    let trimmingTime = -start.timeIntervalSinceNow - 2

    let hours = Int(trimmingTime)/3600
    let minutes = (Int(trimmingTime)/60)%60
    let seconds = Int(trimmingTime)%60
    let milliseconds = Int((trimmingTime - Double(Int(trimmingTime))) * 1000)
    let trimmingTimeString = String(format:"%02i:%02i:%02i.%03i", hours, minutes, seconds, milliseconds)

    #if os(OSX)
    XCUIApplication().typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: [])
    #else
    guard let screenshotsDir = screenshotsDirectory else { return }

    let trimmingFlagPath = screenshotsDir.appendingPathComponent("trimmingFlag.txt")

    do {
        try trimmingTimeString.write(to: trimmingFlagPath, atomically: false, encoding: String.Encoding.utf8)
    } catch let error {
        print("Problem setting recording flag: \(trimmingFlagPath)")
        print(error)
    }

    #endif
}

After we called plot in snaptake we finally are going to stop recording in snaptakeStop. We are doing so by removing the recordingFlag we added earlier in snaptakeStart.

class func snaptakeStop(_ recordingFlagPath: URL) {
    let fileManager = FileManager.default

    do {
        try fileManager.removeItem(at: recordingFlagPath)
    } catch let error {
        print("Problem removing recording flag: \(recordingFlagPath)")
        print(error)
    }
}

Finally add the following test function within SnaptakeUITests file. The function contains our plot where our button is simply tapped.

func testExample() {
    snaptake("testExample") {
        XCUIApplication().buttons["button"].tap()
    }
}

Setting up fastfile, gemfile and snapfile

After your UITests are fully set up we need to add related logic outside of XCode. Within your Gemfile in your fastlane folder add gem "listen". Within your Snapfile remove output_directory("./screenshots"). Now we are ready to create a videos lane in your Fastfile. The videos lane is more or less self-explaining. The most relevant part is the recordingListener. Within its handlers the video reording process is started and stopped, when the recordingFlag is added or removed. When recording is stopped, the trimming time for the resulting video is read from our trimmingFlag and stored in trimming_time_dictionary. sh("cd .. && fastlane snapshot --concurrent_simulators false && cd fastlane") builds Snaptake and runs SnaptakeUITests, so our recordingListener could actually be triggered. After recording any videos, they are trimmed and reencoded.

desc "Generate new localized videos"
lane :videos do |options|

  ### RECORDING VIDEOS

  # Delete all existing videos
  mp4_file_paths = Find.find('screenshots').select { |p| /.*\.mp4$/ =~ p}
  for mp4_file_path in mp4_file_paths
    File.delete(mp4_file_path)
  end

  # Ensure that caching folder for screenshots and recording flags exists
  Dir.mkdir(File.expand_path('~/Library/Caches/tools.fastlane/screenshots')) unless Dir.exist?(File.expand_path('~/Library/Caches/tools.fastlane/screenshots'))

  # Setup listeners for starting and ending recording
  fastlane_require 'listen'
  path = nil
  process = nil
  trimming_time_dictionary = {}
  recordingListener = Listen.to(File.expand_path('~/Library/Caches/tools.fastlane/screenshots'), only: /\.txt$/) do |modified, added, removed|
    if (!added.empty?) && File.basename(added.first) == 'recordingFlag.txt'
      recording_flag_path = added.first
      path = File.read(recording_flag_path)
      process = IO.popen("xcrun simctl io booted recordVideo '#{path}'") # Start recording of current simulator to path determined in recordingFlag.txt
    end
    if (!removed.empty?) && File.basename(removed.first) == 'recordingFlag.txt'
      pid = process.pid
      Process.kill("INT", pid) # Stop recording by killing process with id pid
      trimming_flag_path = File.expand_path('~/Library/Caches/tools.fastlane/screenshots/trimmingFlag.txt')
      trimming_time = File.read(trimming_flag_path)
      trimming_time_dictionary[path] = trimming_time # Storing trimming time determined in trimmingFlag.txt for recorded video (necessary due to initial black simulator screen after starting recording)
    end
  end

  # Build SnaptakeUITests and Snaptake and run UITests
  recordingListener.start
  sh("cd .. && fastlane snapshot --concurrent_simulators false && cd fastlane")
  recordingListener.stop

  ### EDIT VIDEOS

  sleep(3)

  # Trim videos and reencode
  mp4_file_paths = Find.find('screenshots').select { |p| /.*\.mp4$/ =~ p}
  for mp4_file_path in mp4_file_paths

    trimmed_path = mp4_file_path.chomp('.mp4') + '-trimmed.mp4'
    trimming_time = trimming_time_dictionary[mp4_file_path]
    sh("ffmpeg -ss '#{trimming_time}' -i '#{mp4_file_path}' -c:v copy -r 30 '#{trimmed_path}'") # Trimming the Beginning of the Videos
    File.delete(mp4_file_path)

    final_path = trimmed_path.chomp('-trimmed.mp4') + '-final.mp4'
    sh("ffmpeg  -i '#{trimmed_path}' -ar 44100 -ab 256k -r 30 -crf 22 -profile:v main -pix_fmt yuv420p -y -max_muxing_queue_size 1000 '#{final_path}'")
    File.delete(trimmed_path)
  end
end

Running videos lane

By calling fastlane videos we are creating our test video:

Call to action

You want to know what is possible with this procedure?

Checkout Bonprix with your iPhone in the (e.g. German) Appstore!

You want to solve similarly exciting technical questions?

Join us at apploft!

snaptake's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

scytalion

snaptake's Issues

Recording fails with "We could not allocate necessary AV objects to perform the recording"

I tried to use snaptake to record a video of my app. Generating the recordingFlag.txt works, but starting to record the video does not.

The error is the following when running IO.popen("xcrun simctl io booted recordVideo '#{path}'") inside the Listen.to-block:

Couldn't create an asset writer for writing video frames: allocationErrorError starting video recorder: Error Domain=simctl.SimulatorError Code=2 "We could not allocate necessary AV objects to perform the recording.".
An error was encountered processing the command (domain=simctl.SimulatorError, code=2):
We could not allocate necessary AV objects to perform the recording.

If I remove Listen.to and just manually start recording the same way within fastlane it works. Is that a permission problem because of different processes? Did you ever stumbled across this?

I found this when researching the problem: https://developer.apple.com/forums/thread/131926 and https://twitter.com/thesunshinejr/status/1245730408282677255 but it seams no one was able to fix that.

Here is the full output:

[21:06:06]: ▸ UISnapshots
/Users/ChaosCoder/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/fastlane-2.168.0/credentials_manager/lib/credentials_manager/appfile_config.rb:34: warning: conflicting chdir during another chdir block
/Users/ChaosCoder/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/fastlane-2.168.0/credentials_manager/lib/credentials_manager/appfile_config.rb:34: warning: conflicting chdir during another chdir block
[21:06:15]: Recording video now in screenshots/de-DE/iPhone 12 Pro-Preview.mp4
/Users/ChaosCoder/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/fastlane-2.168.0/credentials_manager/lib/credentials_manager/appfile_config.rb:34: warning: conflicting chdir during another chdir block
/Users/ChaosCoder/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/fastlane-2.168.0/credentials_manager/lib/credentials_manager/appfile_config.rb:34: warning: conflicting chdir during another chdir block
Couldn't create an asset writer for writing video frames: allocationErrorError starting video recorder: Error Domain=simctl.SimulatorError Code=2 "We could not allocate necessary AV objects to perform the recording.".
An error was encountered processing the command (domain=simctl.SimulatorError, code=2):
We could not allocate necessary AV objects to perform the recording.
[21:06:21]: ▸     ✓ testPreview (15.640 seconds)
/Users/ChaosCoder/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/fastlane-2.168.0/credentials_manager/lib/credentials_manager/appfile_config.rb:34: warning: conflicting chdir during another chdir block
[21:06:21]: ▸ 	 Executed 1 test, with 0 failures (0 unexpected) in 15.640 (15.643) seconds
[21:06:21]: ▸ 
/Users/ChaosCoder/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/fastlane-2.168.0/credentials_manager/lib/credentials_manager/appfile_config.rb:34: warning: conflicting chdir during another chdir block
[21:06:21]: Video recorded in screenshots/de-DE/iPhone 12 Pro-Preview.mp4
/Users/ChaosCoder/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/fastlane-2.168.0/credentials_manager/lib/credentials_manager/appfile_config.rb:34: warning: conflicting chdir during another chdir block
/Users/ChaosCoder/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/fastlane-2.168.0/credentials_manager/lib/credentials_manager/appfile_config.rb:34: warning: conflicting chdir during another chdir block
[21:06:22]: ▸ 2020-11-28 21:06:22.147 xcodebuild[14107:10155506] [MT] IDETestOperationsObserverDebug: 21.744 elapsed -- Testing started completed.
[21:06:22]: ▸ 2020-11-28 21:06:22.148 xcodebuild[14107:10155506] [MT] IDETestOperationsObserverDebug: 0.000 sec, +0.000 sec -- start
[21:06:22]: ▸ 2020-11-28 21:06:22.148 xcodebuild[14107:10155506] [MT] IDETestOperationsObserverDebug: 21.744 sec, +21.744 sec -- end
[21:06:23]: ▸ Test Succeeded

(I get a lot of chdir-warnings, but these are not the problem, I think.)

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.