Tracking AVPlayer starting stream playback

If you ever worked with streaming audio on iOS you definitely used AVAudioPlayer or AVPlayer as one of the low-hanging and robust options provided by Apple.

AVPlayer

Outside of the stellar performance and versatility the problem with both of them is a weak delegation system, especially for medium/pro use. It's true that you can go far with AVPlayer and even use its basic status changes to react to its behavior but they have few limitations:

  1. You actually have to work with them via KVO (Key-Value Observation) which is not ideal
  2. KVO still doesn't give you answers to everything you need

I had this basic need for a while: do some stuff in the app once the AVPlayer starts playing music from a stream. KVO'ing 'status', 'rate' and 'currentItem.status' was advised everywhere but none of them worked properly. And relying on AVPlayer's 'readyToPlay' status was incorrect as well - the player changes its status to it as soon as possible and not when actually starting playing the stream.

Thankfully I got to know about another key to look after, the 'currentItem.loadedTimeRanges'. And here's how you use it:

First, you add the observer (and then don't forget to remove it at the end of the player's lifecycle!):

avPlayer.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context: nil)

And then you just observe it!

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if object as? AVPlayer === avPlayer && keyPath == "currentItem.loadedTimeRanges" {
            if let timeRanges = avPlayer.currentItem?.loadedTimeRanges, timeRanges.count > 0 {
                let timeRange = timeRanges.first as! CMTimeRange
                let currentBufferDuration = CMTimeGetSeconds(CMTimeAdd(timeRange.start, timeRange.duration))
                let duration = avPlayer.currentItem!.asset.duration
                let seconds = CMTimeGetSeconds(duration)
                
                if currentBufferDuration > 2 || currentBufferDuration == seconds {
                    delegate.startedPlayingStream()
                }
                
            }
}

Next the player calls its delegate when the stream actually started playing. Neat! πŸ™‚

How to search in Firebase

In case you've been wondering (I was) how to seach for stuff in Firebase's Cloud Firestore I found it randomly today and would like to share it:

Firebase-search

How to increase upload file size limit

I'm using a self-hosted podcast web app (PodcastGenerator) to serve and listen audiobooks in Overcast with Smart Speed and position tracking. In order to get new books in my podcast feed I upload them via the built-in web admin panel. But since the audiobook files can be as large as 200-300MB, uploading them via a browser hits the default upload file limit of both Nginx (my http server) and PHP (which my podcast web app uses) pretty soon.

Increase-upload-file-size-limit

In order to increase the upload file size limit, do this:

  1. Edit Nginx's config
    sudo nano /etc/nginx/nginx.conf
    and add client_max_body_size 2000M; into the 'http' block
    Save and exit

  2. Edit php's config
    sudo nano /etc/php/7.1/fpm/php.ini (or track down your php.ini file by creating a *.php file with contents and run it)
    Find and change upload_max_filesize and post_max_size as well to = 2000M

  3. Finally restart Nginx (or your http server) via
    service nginx reload
    and also you can restart php to be safe via
    service php7.0-fpm restart

All my commands are meant for Ubuntu I'm running, for your OS you'll have use your own alternatives.

Happy uploading! πŸ™‚

It's not worth securing in-app purchases

Few years ago, while working on one iOS app we had a problem with in-app piracy. At that time there were and now there are several ways of keeping track of users' purchases on your own server and validate each of them to stop pirates of using your app's unlockable features with their faked unauthorized purchases.

In-apps

But I remember it was a hassle to build such a validating system. And I also remember reading about even bigger companies discontining their own billing systems like that because it was not worth the hassle for dealing and supporting 95% fair users and their purchases because of 5% scammers.

In order to hack in-apps, your iPhone has to be jailbroken. Then you can use a manual like this to get an in-app for free. The latest info on the amount of jailbroken iPhones I found was from the end of 2016 with only 0.4% of iOS devices being jailbroken which might have the potential to hack anything. From what I’ve read and heard over the years that percentage may be even lower now as less and less people jailbreak, because the main reason for it (I can confirm myself) was to get new functionality. And with the latest few iOS updates we got so much new stuff, there are almost no reasons left to jailbreak, at least not for the new features.

iOS devs still can pass receipts and validate them on servers, but what would be the main reason for it? Receipts are passed and then matched for non-consumable purchases: the user unlocked a part of the functionality once and then when the user restores it, his receipt is matched with the receipt stored on the server to avoid unauthorized functionality unlocks. So if someone can fake unlocking non-consumables you either can build a wall around it... or just give it away. If a person tries to hack your purchase - he's definitely not going to buy it, so just embrace it! Replace a mandatory in-app with free with ads model, or offer even more compelling features in your app and introduce non-hackable subscriptions!

If you sell consumables - even better, there's even less reason to secure them. A person who buys a consumable uses it right away, essentially not leaving much to himself anyway.

As for Android - that’s another story. On Android I found at least one, two and three articles on ways how to hack in apps even without root (analog of jailbreaking).

Fix AirPrint -9923 scanner error on MacOS Mojave

After upgrading to MacOS Mojave last autumn I actually had no problem using my Brother DCP-L2532DW via AirPrint. But everything changed when 10.14.3 rolled out few weeks ago.

AirPrint

My Mac did see the printer on the network but when I tried to scan it gave me the -9923 error and obviously nothing happened afterwards.

The solution was actually simple: to turn off IPv6 in my printer's settings, switching it to IPv4 only. If your printer doesn't have settings, try adjusting your router's settings to disable IPv6 and have each wireless devices (including the printer) to obtain an IPv4-only IP address.

Now I wonder when IPv6 becomes reliable enough for regular household items πŸ™‚

How to merge mp3s on Mac

In case you have a bunch of mp3 files (chapters of an audiobook for example) which you want to merge and keep or listen as a single file here's a quick hint for you:

  1. First you open Terminal and cd to the folder with files
  2. Then you just type `cat file1.mp3 file2.mp3 file3.mp3 > outputfile.mp3' and hit Enter!

Merge-mp3

You will get a big mp3 file which unfortunately will have an incorrect duration from the first file of the batch. But you can easily fix that with ffmpeg or other utilities. I prefer a small GUI apps without installing any other libraries. My app of choice is the MP3 Scan + Repair App. You just open the merged mp3 file with it and push the wrench/fix button. That's about it!

Apple's March Keynote Summary

What has been announced:

Apple News+ - $9.99 subscription for accessing 300+ paper-to-digital magazines, including LA Times and WSJ.

Apple-March-Keynote

Apple Card - virtual credit card with real daily cash cash-backs and a physical credit card made out of titanium 😍 They are actually having nice promises about the low transaction and international (really interesting) fees.

Apple Arcade - gaming subscription with 100+ iOS/Mac/AppleTV exclusive games. Details and pricing info will be later.

Apple TV - new app, with better suggestions, integrations with all the content providers. Soon available on smart TVs as well.

Apple TV+ ad-free, on demand subscription with unique and exclusive content. More details soon.

Personally I was waiting for better iCloud Storage options πŸ™‚ But overall it was a good and very (next level) emotional keynote of all. The guests made the second half of the keynote very heart-warming.

Fixing wife's headphones

My wife has the OG Beats Studio headphones which were produced even before Apple acquired Beats in 2014. The Studios have good battery life and work great by this day except their earpads didn't last as good:

Old-pads

I was asked to fix them and this is what I did. I went to ebay and ordered earpads copies for the presumably Beats Studio 1.0 from China. But when they arrived a month after, it turned out they are incompatible with the Studios because of different, incompatible frames:

Incompatible-frames

The Studios' frames were a bit different shape and with different latches. Also after tearing the worn earpads it looked like the Studio's frames aren't even removable and were glued to the headphones. Not having other options except throwing away the old earpads and the incompatible new ones I got an idea.

I removed the frames from the new earpads by breaking the frames and taking them out of the earpads' cushions. Then I just started pulling on one cushion onto the Studios' frame and being flexible it actually fitted around the old frame very well! After finishing out the first earpad, I repeat the same with the second and it worked! Now my wife has hear headphones as good as new:

Old-pads

Cleaning a GPU

Recently I bought a used Nvidia GTX 1080 ti for a really good price due to the end of the mining craze and miners dropping off their GPUs for cheap.

GPU-assembled

Since I'm waiting for my case to arrive (~2 months overdue), I can't actually use the card yet, so I decided to clean it up in the meantime.

First I took off the fans with the their shroud:

GPU-no-fans

Then I unscrewed everything I could but still couldn't remove the radiator. It turned out it was just quite stuck to the memory chips, VRMs and the die via the thermal pads and paste. So with a bit more effort I could separate the radiator from the PCB. And this is what I've found underneath, yukk:

GPU-dirty

Next I took some isopropyl alcohol as I was advised by my friend, and started rubbing. A lot. The thermal paste was very easy to remove from the die, but the grease from the leaked thermal pads was all over the place and took me probably an hour to clean. There was also greased and/or burned pieces of dust everywhere - this is what would get in all the places of your GPU after a while. I cleaned those pieces of dust with some pressurized air and thin wipes (to reach tight places between capacitors).

This was the result:

GPU-assembled

In the beginning when I first removed the radiator I took a note on the thermal pads which I found underneath it. From what I saw at that moment, the VRM thermal pads looked to be about 1mm thick, when the thermal pads between the radiator and memory were around 2mm. That's why between cleaning the GPU and putting it back together actually one week passed while I was waiting for new pads and thermal paste to replace those I cleaned off the GPU.

When the thermal paste and pads finally arrived, I applied them and tried to assemble the GPU:

GPU-thermal-pads1

But I couldn't fit the radiator back! Not only it wasn't touching the die, it also wasn't touching the VRM πŸ™‚ The reason was that the pieces of the thermal pad I applied on the memory chips (2mm) where to thick and got in the way of the radiator touching all the elements it's supposed to cool. After deciding not to wait another week for new proper-sized thermal pads, I just put the ones that I had in a plastic bag and rolled them with a glass to 1mm thickness πŸ™‚:

GPU-thermal-pads2

Afterwards I could complete the assembly with the radiator touching all the necessary parts. Now my only concern is whether I did a good job or not and didn't actually break something on the way πŸ˜… Hopefully my case would come soon, and I will update this post.

P.S. For the memory chips I used almost all of the 1x120x20mm Thermal Grizzly's 8W pad cut into pieces, whereas for the VRMs - a slice of the 1x120x20mm Phobya Termopad XT with 7W of heat dissipation. Practically you can get thermal pads capable up to 17W made by Fujipoly, but in our region they impossible to find and they cost few times more than the ones I've bought.
For the die I've applied the Thermal Grizzly Kryonaut advised by my cousin few years ago. I've used this paste before with excellent results and can highly recommend it!

Mac Safari YouTube picture in picture

If you're using Safari on the Mac and you like watching YouTube (who doesn't), you can watch your videos in a floating window by using Mac OS's picture in picture mode.

Safari-PIP

To do so, open any Youtube video, double right click on it and select 'Enter Picture in Picture':
Safari-PIP2

That's it! Now you can move away your Safari window and enjoy your videos over other apps.