Rekam video autotest UI yang bekerja di Chrome tanpa kepala

Halo semuanya!

Pada artikel ini saya ingin berbicara tentang bagaimana tugas merekam tes otomatis video yang bekerja di Chrome tanpa kepala diselesaikan (seharusnya tidak ada masalah dengan perekaman dalam mode non-tanpa kepala). Beberapa pendekatan berbeda akan dipertimbangkan, dan kesulitan serta cara untuk menyelesaikannya akan diberitahukan.

Perumusan masalah

  1. Tes dijalankan di bawah Windows
  2. Tes menggunakan Selenium Web Driver + Headless Chrome
  3. Tes dijalankan di banyak utas

Untuk tes yang jatuh, Anda harus menyimpan video, sementara

  1. Runtime harus tumbuh tidak lebih dari 10%.
  2. Lakukan dengan minimum perubahan dalam implementasi saat ini

Jika Anda tertarik pada solusi, selamat datang di kucing.

Pendekatan naif. Tangkapan layar


Kerangka uji kami memiliki pembungkus tingkat rendah di atas Selenium. Oleh karena itu, implementasi pertama sangat sederhana dan sangat naif: kode ditambahkan ke semua tempat yang mengubah halaman (Klik, Tetapkan kotak teks, Navigasi, dll.), Yang menyimpan tangkapan layar melalui Selenium Web Driver

Driver.TakeScreenshot().SaveAsFile(screenshotPath);

Waktu pelaksanaan uji dengan pendekatan ini telah tumbuh secara signifikan. Alasan: pengoperasian penyimpanan tangkapan layar tidak bekerja sama sekali dengan cepat - dari 0,5 detik hingga beberapa detik.

Screenshot dalam aliran terpisah


Alih-alih kode yang menyimpan tangkapan layar di semua tempat yang mengubah halaman (Klik, Tetapkan kotak teks, Navigasi), kode ditambahkan yang terus-menerus menyimpan tangkapan layar di aliran terpisah

Teks tersembunyi
...
var timeTillNextFrame = TimeSpan.Zero;
while (!_stopThread.WaitOne(timeTillNextFrame))
{
    var screenShotDriver = Driver as ITakesScreenshot;
    if (screenShotDriver == null)
    {
        continue;
    }

    var screenShot = screenShotDriver.GetScreenshot();
    ...
}


Waktu pelaksanaan tes masih sangat lama. Saya tidak mengerti alasan penundaan itu. Kemungkinan besar, Selenium menolak untuk melakukan sesuatu saat tangkapan layar disimpan. Mungkin contoh lain dari Selenium akan membantu, berakhir di sesi yang sama.

Screenshot di utas terpisah melalui Dalang


Itu tidak terlalu menarik untuk melakukan dua contoh Selenium, karena saya sudah lama ingin mencoba tajam dalang dalam praktek - dan di sini saya menemukan alasan yang cocok. Di sisi Selenium, Puppeteer telah dibuat, yang hanya terhubung ke Chrome, sudah dibuat melalui Selenium

Teks tersembunyi
var options = new ConnectOptions()
{
    BrowserURL = $"http://127.0.0.1:{debugPort}"
};

_puppeteerBrowser = Puppeteer.ConnectAsync(options).GetAwaiter().GetResult();


Tes berjalan sendiri melalui Selenium, dan Puppeteer mengambil screenshot di utas terpisah

Teks tersembunyi
...
var timeTillNextFrame = TimeSpan.Zero;
while (!_stopThread.WaitOne(timeTillNextFrame))
{
    var pages = _puppeteerBrowser.PagesAsync().GetAwaiter().GetResult();
    if (pages.Length <= 0)
    {
        continue;
    }
    
    var page = pages[0];
    
    page.SetViewportAsync(new ViewPortOptions
    {
        Width = screenWidth,
        Height = screenHeight
    }).GetAwaiter().GetResult();
    
    var screen = page.ScreenshotStreamAsync().GetAwaiter().GetResult();
    ...
}


Implementasi ini memberikan hasil yang menggembirakan, waktu pelaksanaan meningkat 10%.

Minus

  1. Waktu untuk menyimpan tangkapan layar melalui Puppeteer tidak instan, beberapa frame akan hilang, dan mereka mungkin berubah menjadi sesuatu yang menarik untuk diurai.
  2. Jika Selenium mengganti tab, Anda harus memberi tahu Puppeteer, jika tidak, ia hanya akan screenshot tab pertama dalam kode di atas (mungkin ada cara untuk menemukan tab aktif - Anda perlu melihat).

Perkecil pertama ternyata menjadi pemblokiran bagi saya, jadi kami beralih ke solusi berikutnya.

Screencast


Chrome memiliki fitur menarik - Page.startScreencast . Menurut deskripsi - dia hanya melakukan apa yang diperlukan - dia melemparkan bingkai yang diubah sehingga mereka yang ingin dapat mencegat mereka dan melakukan sesuatu yang menarik dengannya.

Di Selenium dan Dalang, Anda dapat memulai Page.startScreencast, tetapi Anda tidak dapat menambahkan penangan di salah satu atau yang lain. Pelayan hotel sudah menyuarakan - kami sedang menunggu implementasi.

Saya mencoba berteman dengan perpustakaan ChromeDevTools . Sayangnya, saya tidak dapat dengan cepat membangun hubungan baik dengannya. Setelah pencarian lebih lanjut, solusi ditemukan untuk ScreenCast di mafredri / cdp. Navigasi yang tidak perlu dihapus dari contoh asli dan parameter input yang diperlukan ditambahkan:

Teks tersembunyi
package main

import (
    "os"
    "context"
    "fmt"
    "io/ioutil"
    "log"
    "time"
    "flag"

    "github.com/mafredri/cdp"
    "github.com/mafredri/cdp/devtool"
    "github.com/mafredri/cdp/protocol/page"
    "github.com/mafredri/cdp/rpcc"
)

func main() {

    folderPtr := flag.String("folder", "", "folder path for screenshots: example c:\\temp\\screens\\")
    chromePtr := flag.String("chrome", "http://localhost:9222", "chrome connection - example: http://localhost:9222")
    
    widthPtr := flag.Int("width", 1280, "screencast width")
    heightPtr := flag.Int("height", 720, "screencast height")
    qualityPtr := flag.Int("quality", 100, "screencast quality")
    
    flag.Parse()

    if err := run(*folderPtr, *chromePtr, *widthPtr, *heightPtr, *qualityPtr); err != nil {
        panic(err)
    }
}

func run(folder string, chromeConnection string, width int, height int, quality int) error {
    ctx, cancel := context.WithCancel(context.TODO())
    defer cancel()
    
    chromePath := chromeConnection
    folderPath := folder

    devt := devtool.New(chromePath)

    pageTarget, err := devt.Get(ctx, devtool.Page)
    if err != nil {
        return err
    }

    conn, err := rpcc.DialContext(ctx, pageTarget.WebSocketDebuggerURL)
    if err != nil {
        return err
    }
    defer conn.Close()

    c := cdp.NewClient(conn)

    err = c.Page.Enable(ctx)
    if err != nil {
        return err
    }

    // Start listening to ScreencastFrame events.
    screencastFrame, err := c.Page.ScreencastFrame(ctx)
    if err != nil {
        return err
    }

    go func() {
        defer screencastFrame.Close()

        for {
            ev, err := screencastFrame.Recv()
            if err != nil {
                log.Printf("Failed to receive ScreencastFrame: %v", err)
                os.Exit(0)
            }
            log.Printf("Got frame with sessionID: %d: %+v", ev.SessionID, ev.Metadata)

            err = c.Page.ScreencastFrameAck(ctx, page.NewScreencastFrameAckArgs(ev.SessionID))
            if err != nil {
                log.Printf("Failed to ack ScreencastFrame: %v", err)
                os.Exit(0)
            }

            // Write to screencast_frame-[timestamp].png.
            name := fmt.Sprintf("screencast_frame-%d.png", ev.Metadata.Timestamp.Time().Unix())
            
            filePath := folderPath + name

            // Write the frame to file (without blocking).
            go func() {
                err = ioutil.WriteFile(filePath, ev.Data, 0644)
                if err != nil {
                    log.Printf("Failed to write ScreencastFrame to %q: %v", name, err)
                }
            }()
        }
    }()

    screencastArgs := page.NewStartScreencastArgs().
        SetQuality(quality).
        SetMaxWidth(width).
        SetMaxHeight(height).
        SetEveryNthFrame(1).
        SetFormat("png")
    err = c.Page.StartScreencast(ctx, screencastArgs)
    if err != nil {
        return err
    }

    // Random delay for our screencast.
    time.Sleep(600 * time.Second)

    err = c.Page.StopScreencast(ctx)
    if err != nil {
        return err
    }

    return nil
}


Selanjutnya, file ini dikompilasi oleh perintah:

go build -o screencast.exe main.go

Dan saya bisa menggunakannya dalam solusi C # dengan tes:

Teks tersembunyi
var startInfo = new ProcessStartInfo(screenCastPath)
{
    WindowStyle = ProcessWindowStyle.Minimized,

    Arguments = $"-folder={_framesFolderPath} " +
                $"-chrome=http://localhost:{_debugPort} " +
                "-width=1024 " +
                "-height=576 " +
                "-quality=0"
};

Process.Start(startInfo);


Aliran terpisah untuk merekam tangkapan layar dibuang sebagai tidak perlu. Algoritma kerja adalah sebagai berikut:

  1. Mulai Chrome melalui Selenium
  2. Kami mulai Screencast melalui biner yang dirangkai - itu terhubung ke Chrome dan mulai menyimpan aliran bingkai ke folder yang kami tentukan
  3. Di akhir tes, tutup Chrome - biner ditutup secara otomatis
  4. Jika tes gagal - buat video
  5. Kami membersihkan folder dengan bingkai

Pendekatan ini memberikan hasil terbaik dalam hal waktu eksekusi (praktis tidak ada penundaan). Selain itu, ia memberikan informasi maksimum pada tes (praktis tidak ada frame yang hilang).

Kekurangan

1. Resolusi rendah untuk screencast. Jika Anda menjalankan tes dalam beberapa aliran dan mengatur resolusi ke 2560 * 1440 untuk Chrome, maka buffer yang dialokasikan untuk transfer data akan meluap .

2. Dengan peningkatan resolusi, beban pada CPU meningkat.

Akibatnya, di bawah screencast, saya memilih resolusi 1024 * 576 - pada resolusi ini tes bekerja dengan baik dalam 6 utas, prosesor bekerja dalam mode nyaman (6 inti i7-5820).

Kami mengumpulkan video


Tetap mengumpulkan frame dalam video. Untuk ini, saya menggunakan perpustakaan SharpAvi

Teks tersembunyi
private void GenerateVideoFromScreens(string videoPath)
{
    try
    {
        var videoWriter = new AviWriter(videoPath) { FramesPerSecond = 1, EmitIndex1 = true };

        var videoStream = videoWriter.AddMotionJpegVideoStream(1024, 576);

        var screens = new DirectoryInfo(_framesFolderPath).GetFiles().OrderBy(f => f.CreationTimeUtc.Ticks).ToList();
        foreach (var screen in screens)
        {
            try
            {
                using (var bmp = new Bitmap(screen.FullName))
                {
                    var bits = bmp.LockBits(new Rectangle(0, 0, videoStream.Width, videoStream.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppRgb);
                    var videoFrame = new byte[videoStream.Width * videoStream.Height * 4];
                    Marshal.Copy(bits.Scan0, videoFrame, 0, videoFrame.Length);
                    bmp.UnlockBits(bits);

                    videoStream.WriteFrameAsync(
                        true,
                        videoFrame,
                        0,
                        videoFrame.Length).GetAwaiter().GetResult();
                }
            }
            catch(Exception ex)
            {
                // ignore all video related errors per frame
            }
        }

        videoWriter.Close();
    }
    catch
    {
        // ignore all video related errors per streams
    }
}


Gambar Kelas Atas


Karena resolusi screencast sangat kecil 1024 * 576, Anda perlu mengatur resolusi kecil untuk Chrome itu sendiri, jika tidak, akan ada masalah dengan teks kecil.

Chrome 2560 * 1440 -> screencast di 1024 * 576 = teks kecil praktis tidak dapat dibaca
Chrome 1920 * 1080 -> screencast di 1024 * 576 = teks kecil dibaca dengan susah payah
Chrome 1408 * 792 -> screencast di 1024 * 576 = teks kecil dibaca tanpa masalah

Video yang dihasilkan 1024 * 576 dapat ditingkatkan - jika bingkai diperkecil hingga 1920 * 1080 menggunakan perpustakaan PhotoSauce

Teks tersembunyi
public Bitmap ResizeImage(Bitmap bitmap, int width)
{
	using (var inStream = new MemoryStream())
	{
		bitmap.Save(inStream, ImageFormat.Png);
		inStream.Position = 0;
		using (MemoryStream outStream = new MemoryStream())
		{
			var settings = new ProcessImageSettings { Width = width };
			MagicImageProcessor.ProcessImage(inStream, outStream, settings);
			return new Bitmap(outStream);
		}
	}
}


Hasilnya, diperoleh parameter berikut: Chrome berfungsi di 1408 * 792, ScreenCast di 1024 * 576, video terakhir untuk melihat peningkatan ke 1920 * 1080. Klik di sini untuk melihat contoh hasil akhir.

terima kasih


Terima kasih kepada semua orang yang telah membaca - jika ada solusi yang lebih sederhana untuk masalah aslinya, silakan tulis di komentar. Setiap kritik juga diterima, termasuk kritik jahat seperti yang dijelaskan di atas.

Semua kesehatan dan cepat untuk pembatasan yang didambakan!

All Articles