Category Archives: Blog

Main blog category, I’m too lazy to manage multiple categories so this blog has only one category.

Apple AirPods Pro vs Sony WH-1000XM4 vs Samsung Galaxy Buds+

不废话,来个经典的优缺点列表式测评

Apple AirPods Pro

优点

  • 优质的 ANC(主动降噪)效果,主管感受上基本打平 Sony
  • 无敌的 Transparency Mode,在目前所有的 TWS 中是最强的,穿透进来的环境声音不强不弱,大部分情况下确实可以达到「transparent」的水准
  • 小巧便携,放进裤兜里即可
  • 与 Apple 设备高度集成,体验流畅

缺点

  • 续航短,开启 ANC 基本就 3 个小时左右,只适用于国内航班
  • 不支持多设备同时在线(类似蓝牙 Multipoint 的特性)
  • 低频表现一般

Sony WH-1000XM4

优点

  • 手感、做工不错
  • 超长续航,开启 ANC 也可以有 30 小时的续航,轻松应对国际航班/一整天的工作环境
  • 双设备同时在线
  • 可以 3.5 mm 有线连接

缺点

这款耳机的很多优点也都是它的缺点,比如:

  • 虽然支持超长续航,但没有无线充电或 Qi 充电
  • 支持双设备同时在线,但仅支持双设备,不支持更多设备,也不支持多设备快速切换。并且开启双设备同时在线后,无法开启 LDAC,这点很重要,没有 LDAC 的 1000XM4 音质真的只能用渣来形容,然而这一点几乎所有的 KOL、测评都不会提及
  • 用 3.5 mm 有线连接后,如果这时候你拔出接头,耳机会执行一次关机,不知道这样设计的逻辑是什么?导致我每次关机后都需要再手动开机然后蓝牙配对
  • 耳机上执行上划/下划切换音量时并没有绑定系统音量。也就是说这个切换不会同步设备中的系统音量

除此之外还有以下缺点:

  • 佩戴不舒适,真不知道是不是索尼在国内的水军太多了,还是我的头部构造离谱。我佩戴超过 2 小时后必头疼,夹耳朵,无论怎样调整都夹耳朵
  • 环境音模式(类似 AirPods Pro 的 Transparency Mode)效果很一般,开启低等级听不清人声,开启高等级后底噪高的离谱。可能全封闭式耳机想要达到入耳式耳塞的「透明模式」的效果确实更难一些

Samsung Galaxy Buds+

作为这几款中最便宜的耳机,也是唯一没有主动降噪的耳机。不能要求过多:

优点

  • 价格便宜
  • 续航略优于 AIrPods Pro
  • 但在 Galaxy Wearable 中,可以调出「超高环境音量」模式,可以将环境音以高于正常的音量传入耳朵。很有意思,感觉像带了耳返

缺点

  • 没有 LDAC
  • 由于它造型的原因,无法或较难单手打开电池仓
  • 与 1000XM4 一样操作音量时不会绑定系统音量,不过这个默认是关闭的,在「实验室功能」里

The Simplest MediaWiki Update Script for Single-Server MediaWiki Site

System requirements:

  • User uploads $wgUploadDirectory are stored offsite
  • Non-Docker MediaWiki with normal setup
  • Composor installed (Can be installed automatically during updating)

Goals:

  • Update MediaWiki with nearly zero downtime
  • Download and install latest tagged MediaWiki from tarball package
  • Update extensions and skins from latest git tagged branch
  • Install extension-specific dependencies during updating

First create a script name it mw-update.sh and make sure the script exits if anything goes wrong:

# Exit the whole script if anything goes wrong
set -e

Create a config file mw-update.conf with the following content:

version=1.35.1
tmp="/tmp"
dest_base="/srv/www/wikiroot"
dest="/public_html"
permissions="nginx.nginx"
extensions="
BetaFeatures
CheckUser
CommonsMetadata
MobileFrontend
cldr
Flow
ContributionScores
intersection
"
skins="
MinervaNeue
"

Switch back to your script. Add more variables to use later:

MWU_START=`date +%s`

# Set the current working directory to the directory of this script
cd ${0%/*}

# Check if config exists
if [ ! -f ./mw-update.conf ]; then
  echo -e "Config not found, run the following command first:"
  echo -e "\n$ cp mw-update.sample.yml mw-update.yml"
  exit 1
fi

# Parse config
. mw-update.conf

# MediaWiki uses git tag for latest stable version
MW_VER=$version

# Decoration
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# Production dir
PROD_BASE="$dest_base"
PROD_DIR="$PROD_BASE$dest"

# `MW_VER_MAIN` is for the stupid download URL
MW_VER_MAIN=$(echo $MW_VER | sed -E 's/\.[0-9]+$//g')

# Temp dir prepare for update
TMP_BASE="$tmp/mediawiki-update"
TMP_DIR="$TMP_BASE/mediawiki-$MW_VER"

# Extensions dir
EXT_DIR="$TMP_DIR/extensions"
SKIN_DIR="$TMP_DIR/skins"

# Extensions use git branch for latest stable version
EXT_VER=$(echo REL$MW_VER_MAIN | sed -E 's/\./_/g')

# List of custom extensions
EXTENSIONS="$extensions"
SKINS="$skins"

Then print the setup info for confirmation:

# Prompt before executing anything
echo -e "${BLUE}MediaWiki Auto Updater${NC}"
echo -e "Tunghsiao Liu ([email protected])\n"
echo -e "     Core version: ${BLUE}$MW_VER${NC}"
echo -e "   Branch version: ${BLUE}$MW_VER_MAIN${NC}"
echo -e "Extension version: ${BLUE}$EXT_VER${NC}"
echo -e "   Temp directory: ${BLUE}$TMP_BASE${NC}"
echo -e " Destination base: ${BLUE}$PROD_BASE${NC}"
echo -e "      Destination: ${BLUE}$PROD_DIR${NC}"
echo -e "      Permissions: ${BLUE}$permissions${NC}"
echo -e "Custom extensions: ${BLUE}$EXTENSIONS${NC}"
echo -e "     Custom skins: ${BLUE}$SKINS${NC}"

Continue executing if user confirms:

read -p "Press enter to continue..."
echo -e "\n"

Check if PHP Composor installed:

function check_composer() {
  # Allow running Composer with root
  export COMPOSER_ALLOW_SUPERUSER=1

  if command -v composer >/dev/null 2>&1 ; then
    echo -e "${BLUE}PHP Composer found: $(composer --version  | head -n 1)${NC}"
  else
    echo -e "PHP Composer not found, trying to install it..."
    php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
    php composer-setup.php
    php -r "unlink('composer-setup.php');"
    echo -e "Move PHP Composer to /usr/local/bin/composer for global use"
    mv composer.phar /usr/local/bin/composer
  fi
}

Begin to update MediaWiki cores:

# Update core files
echo -e "${BLUE}Creating essential directories${NC}"
mkdir -p $TMP_BASE

echo -e "${BLUE}Updating MediaWiki core files${NC}"
cd $TMP_BASE
wget -c https://releases.wikimedia.org/mediawiki/$MW_VER_MAIN/mediawiki-$MW_VER.tar.gz
tar -zxf mediawiki-$MW_VER.tar.gz

echo -e "${BLUE}Backing up LocalSettings.php${NC}"
cd $TMP_DIR
cp $PROD_DIR/LocalSettings.php $TMP_DIR/

Update extensions:

# Update extensions
for extension in $EXTENSIONS
do
  echo -e "${BLUE}Updating extension $extension...${NC}"
  cd $EXT_DIR
  if [ ! -d "$EXT_DIR/$extension" ]; then
    echo -e "${EXT_DIR}/${extension} git repo does not exists!"
    git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/$extension.git
  fi
  cd $EXT_DIR/$extension
  git reset --hard
  git clean -f -d
  git pull
  git checkout $EXT_VER
  echo ""
done

Some extensions need special setup process like update git submodules or install dependencies with Composor:

if [[ ${extensions} =~ "Flow" ]]; then
  echo -e "${BLUE}Updating composer for Flow (StructuredDiscussions)...${NC}"
  cd $EXT_DIR/Flow
  check_composer
  composer update --no-dev
  echo ""
fi

Update skins:

# Update skins
for skin in $SKINS
do
  echo -e "${BLUE}Updating skin $skin...${NC}"
  cd $SKIN_DIR
  if [ ! -d "$SKIN_DIR/$skin" ]; then
    git clone https://gerrit.wikimedia.org/r/mediawiki/skins/$skin.git
  fi
  cd $SKIN_DIR/$skin
  git reset --hard
  git clean -f -d
  git pull
  git checkout $EXT_VER
  echo ""
done

Please note that all the above process are done in $TMP_BASE directory. The website is still untouched at the moment. Now lets go into $TMP_BASE to finalize the prepare process:

cd $TMP_BASE

echo -e "${BLUE}Backing up old files...${NC}"
# tar mediawiki-$MW_VER-update-backup.tar.gz -C $PROD_DIR .
mv $PROD_DIR $PROD_BASE/mediawiki-$MW_VER-backup-$(date +%F-%H:%M)

echo -e "${BLUE}Moving updated files to production...${NC}"
cp -R $TMP_DIR $PROD_DIR

echo -e "${BLUE}Fixing directory permissions...${NC}"
chown -R $permissions $PROD_DIR

echo -e "${BLUE}Running MediaWiki maintenance...${NC}"
php $PROD_DIR/maintenance/update.php --quick

MWU_END=`date +%s`
MWU_RUNTIME=$((MWU_END - MWU_START))

echo -e "${BLUE}Done! Time took: ${MWU_RUNTIME}s${NC}"

GeoIP Bypassing for Nginx Proxy

Goal:

  • Proxy content for requests in specific country or region
  • Redirect any requests made outside specific country or region to original URL (to save bandwidth
geoip_country         /usr/share/GeoIP/GeoIPv6.dat;
map $geoip_country_code $proxy_direct_pass {
  default yes;
  CN no;
}

location ~* ^/proxied-content/(.*)$ {
  if ($proxy_direct_pass = yes) {
    return 302 https://original_content/$1$is_args$args;
  }

  proxy_pass https://original_content/$1$is_args$args;
}

Proxying and Caching WebP Images Using the Same URI Based on User Accept Headers with Nginx

Case:

  • The proxied image backend serves WebP images when the client requests support it with Accept headers ($http_accept)
  • The backend also provides the same URI for all WebP requests. That means URI like image.png can also be in WebP format

The solution:

  • Using Nginx map module
  • Apply variables to different cache pools

In nginx.conf:

# Proxy cache pools for image caching
proxy_cache_path        /dev/shm/image_cache
                        keys_zone=image_cache:10m;

proxy_cache_path        /dev/shm/image_cache_webp
                        keys_zone=image_cache_webp:10m;

# Differenate WebP requests
map $http_accept $webp_pool {
  default                 image_cache;
  ~*webp                  image_cache_webp;
}

In your site config:

proxy_cache             $webp_pool;

Use IKEA TRÅDFRI Driver with HomeKit

You must use IKEA TRÅDFRI Gateway to add your drivers. Philips Hue Bridge can add TRÅDFRI drivers but they won’t appear in your Home app. That means TRÅDFRI drivers with Philips Hue Bridge won’t work with HomeKit. So dig into your pockets to buy a TRÅDFRI Gateway.

After you setting up your TRÅDFRI Gateway with IKEA Home smart app. The app will force you to add a controller (Wireless Dimmer, On-Off Switch/Dimmer, etc.) to control any smart accessory. That prevents you from adding any driver directly to the app. You have to add the driver through a controller. So again, dig deeper into your pockets to buy a switch/dimmer. I recommend TRÅDFRI Wireless dimmer. I just bought one as the device adoptor since I won’t use it to control my accessories.

You can pair the wireless dimmer with more than one drivers. When you push on/off on the dimmer. All paired drivers will respond. It’s okay if you just use it as the device adoptor like me. But if you want to use it later as a normal dimmer. Just remove it from the app and re-pair it the right driver.

Don’t buy any other IKEA smart accessories at the time of writing. Like motion sensors, bulbs, etc. Blinds are okay but reported the motors are louder than other brands. That means most of them suck.

Update Mar 8, 2021: Recently IKEA announced that they add HomeKit support for existing TRÅDFRI Shortcut Buttons and TRÅDFRI Motion Sensors. Recently I purchased these two products and did some tests:

  • Recommended: TRÅDFRI Shortcut Buttons. It responds faster than Aqara Wireless Mini Switch. It has smaller form compared to Hue switches. Zigbee connection is also stable.
  • Not recommended: TRÅDFRI Motion Sensors. The most recent revision can’t change the detection interval. It only detects motion every 180 seconds. Even worse than Aqara Motion Sensor.

Low Latency HomeKit Camera Support: Homebridge FFmpeg Plugin with Custom Codec Configuration

Stream passthrough method for low latency HomeKit camera support:

  • Device: EZVIZ (萤石) CS-C6CN-3H2CWF (星光夜视版)
  • Low latency (as low as 100ms) from the camera to homebridge-camera-ffmpeg
  • Low CPU load since all streams are passthrough from FFmpeg without re-encoding
  • No audio support due to compatibility issue from the source stream

The configuration should be look like:

"videoConfig": {
    "source": "-rtsp_transport tcp -i rtsp://admin:<device_passcode>@<device_ip>:554/h265/ch1/main/av_stream",
    "maxFPS": 30,
    "audio": false,
    "vcodec": "copy",
}

Here’s the tricky part: the EZVIZ CS-C6CN-3H2CWF will output h265 by default, which is not supported by HAP. And the default bitrate is also too high to handle for iOS devices. So you will need to update some settings for the camera:

  • Download the Chinese version of EZVIZ Studio from the official website (direct link).
  • Install and run
  • Click 本地设备 (Local Devices) on the sidebar
  • Right-click the camera you want to modify, then choose 高级设置 (Advanced Settings)
  • In the popup window, click 图像 (Images) – 视音频 (Audio/Video) on the sidebar, change the following settings:
    • 编码类型 (Encoding Type): STD_H264
    • 码率上限 (Bitrate Limit): 1024 Kbps
    • (Optional) 分辨率 (Resolution): HD720P
  • Click 保存 (Save) to save the settings

Please note this step will slightly reduce the quality of your video stream when you view the stream in EZVIZ official app, but it’s mandatory for the camera to work with HomeKit.

Bonus: audio support without compiling FFmpeg after version 1.0.0:

"videoConfig": {
    "source": "-rtsp_transport tcp -i rtsp://admin:<device_passcode>@<device_ip>:554/h265/ch1/main/av_stream",
    "maxFPS": 30,
    "audio": true
}

This will make FFmpeg re-encode your stream and hurt your CPU. The latency is about 0.6-1 second. I suggest you install Homebridge on a Mac mini for better performance.

Update for homebridge-camera-ffmpeg 3.0.0 and later: It’s finally stable enough to stream passthough with audio support. You can now use the following config:

"videoConfig": {
    "source": "-rtsp_transport tcp -i rtsp://admin:<device_passcode>@<device_ip>:554/h265/ch1/main/av_stream",
    "maxFPS": 30,
    "audio": true,
    "vcodec": "copy",
}

Allow WordPress Embedded Posts with Global X-Frame-Options for Nginx Servers

The problem: when you enables X-Frame-Options globally. You won’t be able to embed your posts with latest WordPress embed posts method.

The solution: you can simply exclude it in your Nginx configuration. I’ll use Nginx map for better performance:

map $request_uri $x_frame_options_headers {
  default                 SAMEORIGIN;
  # Matching WordPress embed page, ie. https://example.com/my-post/embed#?secret=vLi4CQcWkH
  ~/embed                 "";
}

# Don't allow the browser to render the page inside an frame or iframe
add_header X-Frame-Options $x_frame_options_headers;
Embedding Demo