Video Quality on Android

Herkese merhabalar bugün sizlere youtube videolarda gördüğümüz ve kullandığımız görüntü kalitesi ayarını otamatik nasıl yapar ve bunu nasıl yapmayı öğrendiği, yazılım ve teknolojiyle ilgili her sorunun iyi bir cevabı var olduğunu biliyoruz, çünkü yazılımı seviyoruz 👨💻👩💻 Tarifimize başlamadan önce gerekenler listemiz;
- Backend (Spring)
- Mobil (Android)
- FFmpeg (Video Coding)
- Local Server (XAMPP HTTP-Server)
Gerekenler listemizi hazırladıysak video görüntü ayarları bizim için neden önemli olduğunu anlatalım. Bildiğiniz üzere kullanıcılar mobil uygulamalarımızda her zaman Wi-Fi kullanamamakta bundan dolayı da mobil veri bağlantısına ihtiyaç duymaktadır. Bildiğiniz üzere mobil veri bağlantıları 5G gelmesiyle artık çok gelişti ancak benim gibi köy yerlerinde dağlarda bazen çok iyi çekmeyebiliyor ancak mobil internet ile video izlemek isteyebiliyoruz. Eğer video çok yüksek boyutlarda ise donarak video izlemek kullanıcılar için olumsuz etki bırakmakta. Bunun yerine kullanıcıları internet bant genişliğindeki video ile kullanıcıları donmadan da olsa video izlemesini sağlamalıyız. Bu yapıyı Youtube, Facebook, Instagram, Twitch… Birçok şirket hali hazırda kullanmaktadır. Amaçları kullanıcıları aktif tutarak uygulama içerisinde Smooth bir akış sağalamaktır.
Evet, artık neden olduğunu öğrendikten sonra gelelim tarifimize. Öncelikle Backendimizi bir spring boot ile ayağa kaldıralım. Spring’de Java ve Kotlin kullanabilmekteyiz. Ben Kotlin tercih ediyorum, Kotlin ve Java karşılaştırmaları için bakınız. Kotlini dependency modülüne ekleyerek kolayca kurulumunu yapabilirsiniz.
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
Kotlin kurulumunu yaptıktan sonra, Backend’imizde öncelikle video yükleyeceğimiz bir end pointimizi belirliyoruz. “/api/v1/videos” POST işlemiyle bir video yükleyeceğiz. FileUploadService classımızı ”@AutoWired” Anotions ile inject ederek lateinit var şeklinde oluşturduğumuz service Spring senin için bir nesne üretecek ve sen bu nesneyi sonradan atanacaksın bundan dolayı “lateinit var” ile üretiyoruz. “Lateinit” için bakınız. FileUploadService class’ımızdaki uploadFileLocal fonksiyonumuza dosyamızı gönderiyor ve FileUploadService kısmından dönen Boolean cevabına göre kullanıcıya SuccessOrErrorModel olarak oluşturdumuz nesnemizi response olarak dönüyoruz.
Şimdi aldığımız dosyayı FileUploadService class’ımızdaki uploadFileLocal fonksiyonumuzu inceleyelim.
uploadFileLocal fonksiyonumuzda aldığımız file öncelikle mp4 olup olmadığını kontrol ediyoruz. Burada örneğimizde mp4 olarak ilerlemekteyiz. Tabi diğer video modelleri için ayrı Class ve fonksiyonlar kullanmamız gerekecektir FFmpeg için kodlar ayrı olacaktır. Biz dedim gibi en çok kullanılan yapı olan mp4 üzerinden ilerliyor olacağız. Burada gelen file dosyamızı geçici olarak bilgisayarımızda bir klasör açarak kaydediyoruz. Kaydettikten sonra kullanıcımızı bekletmemek için response zamanı uzamaması için Thread Sınıfından miras alan RefactorVideoClass’a file dosyamızın adını gönderiyoruz. Çünkü üst kısımda yaptığımız gibi temp’ olarak açtığımız yer RefactorVideoClass yapımız bilmektedir. Sadece aldığımız video adını göndermemiz ve Thread açarak bu işlemi başlatıyoruz. Kullanıcıya ise “True” olarak dönerek videonun başarılı bir şekilde elimize ulaştığını bildiriyoruz.
Bir process’in birden fazla işi aynı anda yapmasını sağlayan yapılara Thread denir.
Aslında tüm video işlememiz bu kısımda gerçekleşiyor. Burada gördüğümüz gibi 3 değişkenimiz yer almakta. Video adı, segmentlere ayırdığımızda videoların adı, ve son olarak bu segmentleri kaçar saniye olarak video ayıracağımızın default değeri olarak atanmış ve constructor sayesinde bunları değiştirme olanağı sunabilmekteyiz. Gelelim FFmpeg nedir ne işimize yarar ?
FFmpeg, herhangi bir video formatını başka bir video formatına kodeklerini de değiştirerek çevirebilen açık kaynak kodlu ücretsiz bir yazılımdır. FFmpeg, neredeyse tüm ses/görüntü kodeklerini(h264, h265, vp8, vp9, aac, opus, etc.), dosya formatlarını(mp4, flv, mkv, ts, webm, mp3 etc.) hatta tüm streaming protokollerini(http, rtmp, rtsp, hls, etc.) destekler.
FFmpeg ile videolarla neler yapabileceğimizi bu linkten bakabilir deneyebilirsiniz. Örnek olarak bir videoyu gif’e dönüştürebilir, bir videoyu fotoğraflara dönüştürebilir, bir videoyu sadece video ve sese dönüşterebilirsiniz. Bizim örneğimizde videoyu 6 kategoriye bölerek video kalitesini düzenleyeceğiz. FFmpeg buradan indirebilir kendi bilgisyarınıza veya serverınıza yüklemeniz gerekmektedir. Windows için ffmpeg kurduğunuz dosyanın bin yolunu Windows’da Environment variable olarak setlemeniz gerekmektedir. Aksi takdirde backend’te koşacağımız Cmd komutu çalıştırmayacaktır.

Kodumuzu incelemeye devam ettiğimizde Bir ProcessBuilder objesi oluşturuyoruz bu objemizin yaptığı açıklamayı bakalım;
/**
* Constructs a process builder with the specified operating
* system program and arguments. This is a convenience
* constructor that sets the process builder's command to a string
* list containing the same strings as the {@code command}
* array, in the same order. It is not checked whether
* {@code command} corresponds to a valid operating system
* command.
*
* @param command a string array containing the program and its arguments
*/
public ProcessBuilder(String... command) {
this.command = new ArrayList<>(command.length);
for (String arg : command)
this.command.add(arg);
}
İşletim sistemi command’lerden oluşan string bir ArrayList almakta ve bunları koşmaktadır. Koşması için builder.start() fonksiyonumuzu başlatmamız gerekmektedir. Start fonksiyonumuzu başlattıktan sonra hata almamız durumunda normalde Start fonksiyonu IOException atmakta ancak biz while döngüsünü sonsuz yaptığımızdan dolayı redirectErrorStream(true) yaptık bu bize aslında herhangi bir hata olursa bunu command satırında yazmamızı sağlıyor bu sayede döngünün break yapıldığından emin olabilmekteyiz. Default value değeri false’dur. Eğer Command sırasıyla cmd.exe ile command satırımızı açıyoruz. Sonrasında geçici olarak yer alan dosyamızın komutuna giderek Windows Env. dosyamızdaki FFmpeg kullanarak komutumuzu koşuyoruz. FFmpeg Komutumuzu inceleyelim.
"ffmpeg -y -i $videoName -preset slow -g 48 -sc_threshold 0 -map 0:0 -map 0:1 -map 0:0 -map 0:1 -map 0:0 -map 0:1 -map 0:0 -map 0:1 -map 0:0 -map 0:1 -map 0:0 -map 0:1 -s:v:0 1920*1080 -b:v:0 1800k -s:v:1 1280*720 -b:v:1 1200k -s:v:2 858*480 -b:v:2 750k -s:v:3 630*360 -b:v:3 550k -s:v:4 426*240 -b:v:4 400k -s:v:5 256*144 -b:v:5 200k -c:a copy -var_stream_map \"v:0,a:0,name:1080p v:1,a:1,name:720p v:2,a:2,name:480p v:3,a:3,name:360p v:4,a:4,name:240p v:5,a:5,name:144p\" -master_pl_name master.m3u8 -f hls -hls_time $parseDynamicVideoRange -hls_playlist_type vod -hls_list_size 0 -hls_segment_filename \"v%v/$segmentedName%d.ts\" v%v/index.m3u8"
ffmpeg komutu ile başlatıyoruz -y (global) bir komut olup ovveride yapmakta dosya var ise üzerine yazmaktadır. -i ile video url vereceğimizi belirtmekteyiz. -present slow bizim video işleme sürecimizi dile getirmekte yani eğer hızlı işlersek video’da daha düşük boyut ancak görüntü kaybı yaşayabiliriz ancak yavaş yaparsak bu sefer daha iyi kalite daha uzun sürede işleyeceğini söylemekteyiz. Map ile video ve sesleri birleştirmeye sağlıyoruz. Map ile detaylı bilgi bakınız. Diğer önemli yapımız master.m3u8 Aslında kullanıcımıza bu text dosyasını vereceğiz. İleride geleceğiz. hls_time olarak dynamic olarak aldığımız değerdir. Segment File adını yine dynamic olarak değiştirebileceğimiz değeri almaktadır default değeri video dosyasının adıdır. Builder.start() bize geriye bir Process dönmekte bu process inputStream olarak dinleyip her bir koşulan komut satırını ekrana göstermekteyiz bu sırada youtube bir video yüklediğimizde işleniyor gözükmesini bir boolean değişikliği kurarak kullanıcılara video işleniyor yapısı gösterebiliriz. Eğer null değerine ulaşmış ise dosyamız artık işlenmiş olup template olarak yüklediğimiz videoyu silebiliriz ve asıl dosyalarımızı cloud’da veya kendi server üzerinde yükleme işlemini yapabilirsiniz.

Burada aşağıdaki görüntü kalitelerini üretmekteyiz.
- v1080p
- v720p
- v480p
- v360p
- v240p
- v144p

Artık videolarımız işlenmiş şekilde görüntü kalitesine göre ayarladık ve master.m3u8 dosyamızı aldık. Peki şimdi ne olcak bu oluşturduğumuz dosyaları Amazon S3 Bucket servisine veya Google, Azure Storage servislerine ya da kendi backend Storage servisimize yüklüyoruz master.m3u8 url değerini alıp bunu artık video url olarak kullanıcıya dönüyor yada database’e kaydediyoruz. Peki bu master.m3u8'de ne mi var ?

M3u8 file formatı bir HLS(HTTP Live Streaming) olmakla birlikte kullanıcılara canlı bir yayın akışı sağlamasını amaçlamaktadır. Bu dosyamızın içerisinde baktığımızda “Bandwidth” ile karşılaşmaktayız aslında tüm mesele burada yatıyor görüntü ayarını internet bant genişliğinize göre hangi çözünürlüğü vereceğini bu master.m3u8 dosyasına göre url kısmını değiştirmemizi sağlıyor. Burada kullanıcının internet değerine göre hangi video url setlenmesini sağlıyor ayrıca mpd dosyasını da i
HTTP Live Streaming veya HLS , Apple tarafından QuickTime, Safari, OS X ve iOS yazılımının bir parçası olarak uygulanan HTTP tabanlı bir medya akış iletişim protokolüdür. MPEG-DASH’ı andırıyor. Genel akışın küçük HTTP tabanlı dosya indirmelerinin dizisine bölünmesi ile çalışır; her yükleme, genel olarak potansiyel olarak sınırsız aktarım akışının kısa bir bölümünü yükler.
Dosyalarımızın içerisine baktığımızda tam olarak bunu görmekteyiz. Belli bir aktarım kesitlerini zaten üst taraftaki kodlarımızda aktarım akışını kaç saniyelere böleceğimizi yazmıştık. Videomuzun toplam süresi 43 saniye bu da 10'a bölerek yaptık ve karşımızda 5 adet bölünmüş .ts dosyaları karşımıza çıkarmakta.

Peki url istek attığımızda ne olcak aslında bir mp4 dosyası gibi çalışmasını bekliyoruz. Ben localserver olarak XAMPP kullanacağım videolarımın local bir server çalışması için. Burasının aynı bir server’da tuttuğumuz gibi düşünebilirsiniz. Localhost xampp’da localhost:80 portunda çalışmakta. Buradaki master.m3u8 isteğimiz localhost/hls-server/input olmakta. Eğer chroma’da bu url istek attığımızda download yapmakta ancak videomuz yüklenmemekte bunun için HLS koşabilen bir video-player bulmamız gerekiyor chrome üzerinde Native HLS Playback gibi bir extension bulunmakta bunu chorme yükleyerek videomuzu başlatabiliriz.


Artık videomuz yayında :) Şimdi artık bu video url Android Tarafında Exoplayer kullanarak görelim :) O zaman Android tarafına başlayalım.
Öncelikle Exoplayer dependency gradle’mıza ekliyoruz. Exoplayer kütüphanesi Google tarafından geliştirilen açık kaynaklı bir Video-player oynatıcısıdır.
implementation "com.google.android.exoplayer:exoplayer:2.14.0"
implementation "com.google.android.exoplayer:exoplayer-core:2.14.0"
implementation "com.google.android.exoplayer:exoplayer-ui:2.14.0"
implementation "com.google.android.exoplayer:exoplayer-hls:2.14.0"
exoplayer-core
: Core functionality (required).exoplayer-dash
: Support for DASH content.exoplayer-hls
: Support for HLS content.exoplayer-rtsp
: Support for RTSP content.exoplayer-smoothstreaming
: Support for SmoothStreaming content.exoplayer-transformer
: Media transformation functionality.exoplayer-ui
: UI components and resources for use with ExoPlayer.
Bizim uygulamamız bildiğiniz üzere HLS üzerinde olacağından core, ui, hls kütüphanelerini de ekliyoruz. MainActivity’mizdeki ui kısmını çizdirelim.
Burada custom bir exo_player_control_view veriyoruz ve butonlarımızı setliyoruz. Burada UI kısmındaki tüm özelleştirmeleriniz uygulamanızın ana temel renklerine ve özelliklerine göre değişmektedir.
MainActivity’mize gelelim. Öncelikle exoPlayer, dataSourceFactory, trackSelector objelerimizi lateinit var ile oluşturulacağını belirtriyoruz. Burada mediaItem değeri şuanlık Compain Object’de static olarak tutuğumuz HLS_STATIC_URL ile MimeType’ı m3u8 formatında yapiyoruz. Ayrıca trackDialog yapısında Dialog göstererek kullanıcıların hangi kalite seçmek istediğini belirtmesini sağlayacak yapımızı göstereceğiz. OnCreate fonksiyonumuzda dataSourceFactory’i ve eğer trackDialog boş ise initPoupQuality ile dialog’umuzu yüklenimini yapmaktayız.

initPopupQuality fonksiyonumuzda trackSelector currentMappedTrackInfo değerini alıyoruz. Burada alınan video değerinin birden fazla Quality’si olduğunu anlamamızı sağlıyor eğer yok ise fonksiyonumuz return etmekde değilse ayarlar görüntüsünü aktif etmekte. MappedTrakInfo.renderCount kadar fonksiyonumuz koşcak. Bu da kaç tane görüntü kalitemiz var ise o kadar bizim 144, 240, 360, 480, 720, 1080 vardı, ayrıca 1 tane de otomatik seçeneğimiz olmakla beraber 6 tane değerimiz olmakta.

mapped.getTrackGroups dediğimizde eğer bir uzunluğumuz yok ise 0 ise bizim aslında bir render değerimiz olmadığını anlamaktayız. Eğer 0'dan farklı ise evaluate ettiğimiz gibi birden fazla değer gelmekte.

Görülüğü gibi bizim format değerlerimiz gelmiş bulunmakta. Hangi’si ile render ettiği değeri getRenderType() fonksiyonu kullanarak C.TRACK_TYPE_VIDEO ile kıyaslıyoruz ve videomuz hangi value değeriyle renderlandığını alıyoruz. For döngümüzden çıktığımızda value değerimiz 0 olmakta görüyoruz. TrackSelectionDialogBuilder oluşturuyoruz contructor yapısında context,title, oluşturduğumuz trackSelector (trackSelector=DefaultTrackSelector(this) atamıştık) ve son olarak da rendererIndex’imizi gönderiyoruz.
TrackSelectionDialogBuilder(
Context context, CharSequence title, DefaultTrackSelector trackSelector, int rendererIndex)
Ve son olarak TrackSelectionDialog’muzun adlarını düzeltmek için string dosyamızdaki gelen height değerine göre değiştiriyoruz.
<string name="exo_track_resolution_pixel"><xliff:g example="768" id="height">%1$dp</xliff:g></string>
Sonrasında oluşturduğumuz dialog’a atamasını yapıyoruz bu sayede her tıklanmada sürekli aynı işlemlerin yapılmasını engelleyip performans kazanıyoruz.

Bu sayede artık kullanıcın artık video kalitesini seçerek videosun kalitesini ayarlamasını sağladık. Default olarak video kalitesi otomatik olmaktadır. Ancak kullanıcı daha kaliteyi yükseltebilir veya kaliteyi düşürerek takılmadan videoyu izlemesini sağlatabilir ve kullanıcıları uygulamada aktif olarak tutabiliriz. Demo bir video çektim tam mantığını anlatmak için iyi seyiler dilerim umarım tıkanmaz youtube video tıkanmaz alttaki gibi olmak istemeyiz :)
Türkçe anlatım videosu youtube’da yayınladım izlemek isteyenler için linki aşağıya bıraktım.