コグノスケ


link 未来から過去へ表示  link 過去から未来へ表示(*)

link もっと前
2018年6月25日 >>> 2018年7月8日
link もっと後

2018年6月26日

Kindleの本もダブることがある

目次: Kindle

Kindleの本は「ダブって買うことがない」利点があります。

が、例外もあって、今回ハマってしまいました。

ASIN

Amazonの電子書籍には必ずASIN(Amazon Standard Identification Number)というユニークなIDが振られています。AmazonストアのURLの最後に付いている、謎の英数字、あれがASINです。

Kindleストアで本を選択したとき、自動的に購入済みのASINかどうか(=その本を持っているかどうか)チェックされ、購入済みか否かで、ストアに表示されるボタンが変わります。例えば500円の本なら、

  • 未購入、未発売: 「¥500で予約注文」ボタン
  • 未購入、発売中: 「¥500で今すぐ購入」ボタン
  • 購入済み: 「今すぐ読む」ボタン

このように3パターンのうち適切なボタンが表示されます。また、ブラウザの戻る機能で購入ページに戻り、同じ購入ボタンを押しても「あなた、もう買ってるでしょ?」って言われます。Kindleのダブり防止機能はかなり強力です。

この素敵な機能のおかげで、同じ本を2冊買うことはほぼありません。

同じ本、違うASIN

Kindleのダブり防止は「同じ本なら、ずっと同じASIN」という前提に依存していますので、この前提が崩れてしまうと、チェックが効かず、同じ本を2回買えてしまいます。例えば、下記のような場合です。

  • 同じ本がストアに2つ登録されている(ストア側がダブっている)
  • 同じ本がストアに2回登録された(古い本を消して、新しい本を登録した)

私が今回ハマったパターンは後者でした。オンラインThe Comicという本の9巻目で、同じ本のようですが、なぜか ASIN: B07CH794VZ(以降、旧版)と、ASIN: B07CSVJYL2(以降、新版)の2つのASIDが存在しています。そのため、

  • 旧版を買う
  • 旧版がストアから外され、新版が登録される
  • 新版も買う

このような経緯で2冊買ってしまいました。ちなみに現在、旧版は買えず、評価ページだけが悲しく残っています。

返品できる

Amazonのカスタマーサポートに連絡したところ、新版の購入を取り消して、返金してもらうことができました。またカスタマーサポートの方が言うには「ストア内で本の入れ替えはある」ようです。

今回のようなASINの付け替えは良くあるのか?珍しいのか?ちょっと気になります。あまり気軽にASINの付け替えをやられますと、購入、未購入の管理が結構辛くなります。

しかしまあ、今まで6,000冊ほど買ってきて、今回初めてASINの付け替えに当たったので、体感的にそんなに頻度は高くないように思います。普通に楽しむ程度なら、まず困らない頻度だとは思います。

編集者:すずき(2021/12/08 04:01)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2018年6月28日

Namazu延命

Namazuの延命策として、検索文字列にUTF-8文字列を投げられるようにしました。

といっても、オリジナルのCGIをnamazu.cgi → namazu2.cgiにリネームして、下記のラッパーCGIを用意しただけです。

SakuraはPerlが2種類(/usr/bin/perl = perl 5.8, /usr/bin/perl5 = perl 5.14)入っていて、perl 5.14だけText::Iconvがインストールされています。しかしなぜか @INCにパスが通っていません。仕方ないので、かなりダサいですが、強引にパスを通しています。

UTF-8の検索文字列をEUC-JPに変換するラッパー

#!/usr/bin/perl5

use lib '/usr/local/lib/perl5/site_perl/5.14/mach';

use CGI;
use Text::Iconv;

my $cv = Text::Iconv->new("UTF-8", "EUC-JP");
my $cgi = CGI->new;
my $q_cv = $cv->convert(scalar $cgi->param('query'));
$cgi->param('query', $q_cv);
$q_str = $cgi->query_string();
print "Location: namazu2.cgi?" . $q_str . "\n\n";

今回スクリプトを書いていて初めて知ったのですがCGIのparam関数を読んでスカラ値を受けたい場合scalar $cgi->param('query') という書き方をした方が良いそうです。

理由の詳細はこのブログ(New Class of Vulnerability in Perl Web Applications)に載っていますが、ハッシュで受けると意図せず他の値を上書きしてしまう可能性があるので、明示的にスカラとして受け取るべき、ということらしいです。

気づいたきっかけ

Perl初心者の私がなぜ気づいたかというと、動作テストをしたときにApacheの動作ログに、下記のWarningが記録されたからです。

Apacheのエラーログ
AH01215: CGI::param called in list context from /home/katsuhiro/public_html/namazu/aaa.cgi line 8, this can lead to vulnerabilities.
See the warning in "Fetching the value or values of a single named parameter" at /usr/share/perl5/CGI.pm line 412.

初心者にはエラーメッセージだけだと意味がわかりませんが、Warningメッセージに言われるがまま /usr/share/perl5/CGI.pm 412行目を見ると、

CGI.pm

	# list context can be dangerous so warn:
	# http://blog.gerv.net/2014.10/new-class-of-vulnerability-in-perl-web-applications
	if ( wantarray && $LIST_CONTEXT_WARN == 1 ) {
		my ( $package, $filename, $line ) = caller;
		if ( $package ne 'CGI' ) {
			$LIST_CONTEXT_WARN++; # only warn once
			warn "CGI::param called in list context from $filename line $line, this can lead to vulnerabilities. "
				. 'See the warning in "Fetching the value or values of a single named parameter"';
		}
	}

危険な理由として、先ほど紹介したブログのURLを書いてくれていました。親切ですね。ブログを読んだので何となくわかりましたが、説明なしでは若干難解なコードかもしれません…。

編集者:すずき(2018/06/29 00:12)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2018年6月29日

Androidチューナー周りの調査メモ

Android 8のチューナー周りについて、家でも少し調べていたので、わかったことのメモを貼っておきます。環境は下記の通りです。

  • タグ: android-8.1.0_r33
  • ディレクトリ: android/packages/apps/TV

チューナーの動作は大きく分けると、チャンネルスキャン、視聴の2つです。

チャンネルスキャン

チャンネルスキャンは全てのチャンネルに対してチューニングを行って、受信できるかどうか試す動作を指します。Android 8の場合、フレームワーク内部にチャンネルのリストを持っていて、リストの先頭から試す仕組みになっているようです。

コードを見た感じ、処理は下記のように進むようです。

チャンネルスキャン画面の動作
// @ src/com/android/tv/tuner/setup/TunerSetupActivity.java
TunerSetupActivity::executeAction(String category, int actionId, Bundle params)
    switch (category)
      case ConnectionTypeFragment.ACTION_CATEGORY:
        mLastScanFragment = new ScanFragment();
        Bundle args1 = new Bundle();
        // actionIdはGUI画面の選択肢(Antenna, Cable, Not sure)の
        // 上から何番目か?に相当する、一番上がactionId = 0
        // ★★CHANNEL_MAP_SCAN_FILEは後述
        args1.putInt(ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE,
                     CHANNEL_MAP_SCAN_FILE[actionId]);
        // ...snip...
        // ScanFragmentを実行、表示する
        showFragment(mLastScanFragment);

ScanFragment::onCreateView() @ src/com/android/tv/tuner/setup/ScanFragment.java
  ScanFragment::startScan()
    new ChannelScanTask()
    ChannelScanTask::execute()

ScanFragment::ChannelScanTask::doInBackground() @ src/com/android/tv/tuner/setup/ScanFragment.java
  //チャンネルリストを得る
  ChannelScanFileParser::parseScanFile() @ src/com/android/tv/tuner/setup/ScanFragment.java
  ScanFragment::ChannelScanTask::scanChannels()
    TunerTsStreamer::startStream(ChannelScanFileParser.ScanChannel) @ src/com/android/tv/tuner/source/TunerTsStreamer.java
      TunerHal::tune() @ src/com/android/tv/tuner/TunerHal.java
        Java_com_android_tv_tuner_TunerHal_nativeTune() @ jni/tunertvinput_jni.cpp
          DvbManager::tune() @ jni/DvbManager.cpp
            //DVB API v5でのチューニング方法
            ioctl(FE_SET_PROPERTY) // /dev/dvb0.fe0などに対して実施

DvbManager::tune() はATSC決め打ちの場所があって、DVBやISDBなど他の放送規格には対応していないように見えます。

CHANNEL_MAP_SCAN_FILEはスキャンするチャンネルの一覧が書いてあるファイルのリソースIDが並んでいます。

チャンネルリストのリソースID
    private static final int CHANNEL_MAP_SCAN_FILE[] = {
        R.raw.ut_us_atsc_center_frequencies_8vsb,
        R.raw.ut_us_cable_standard_center_frequencies_qam256,
        R.raw.ut_us_all,
        R.raw.ut_kr_atsc_center_frequencies_8vsb,
        R.raw.ut_kr_cable_standard_center_frequencies_qam256,
        R.raw.ut_kr_all,
        R.raw.ut_kr_dev_cj_cable_center_frequencies_qam256,
        R.raw.ut_euro_dvbt_all,
        R.raw.ut_euro_dvbt_all,
        R.raw.ut_euro_dvbt_all
    };

ちなみにファイルはpackages/apps/TV/usbtuner-res/rawの下にあります。もし新しいファイルfile_nameを追加したいときは、ディレクトリの下にファイルを追加し、CHANNEL_MAP_SCAN_FILEにR.raw.file_nameのように、追加すれば良いみたいです。

視聴

TV視聴の際は、チューニングとMPEG2-TSデータを受け取って、セクション解析する処理が行われるようです。

チューニングについては、チャネルスキャンで紹介したTunerHal::tune() が使われますので割愛して、TSデータを受け取るところを調べます。どうも下記のようになっているようです。

DVB APIデバイスからTSを受け取って、PSIパーサーに渡すまで
TunerTsStreamer::StreamingThread::run() @ src/com/android/tv/tuner/source/TunerTsStreamer.java
  TunerHal::readTsStream() @ src/com/android/tv/tuner/TunerHal.java
    TunerHal::nativeWriteInBuffer()
      Java_com_android_tv_tuner_TunerHal_nativeWriteInBuffer() @ jni/tunertvinput_jni.cpp
        DvbManager::readTsStream() @ jni/DvbManager.cpp
          read() // /dev/dvb0.dvr0など
  EventDetector::feedTSStream() @ src/com/android/tv/tuner/tvinput/EventDetector.java
    TsParser::feedTSData() @ src/com/android/tv/tuner/ts/TsParser.java
      TsParser::feedTSPacket()
        Stream::feedData()
          SectionStream::handleData()
            SectionParser::parseSections() @ src/com/android/tv/tuner/ts/SectionParser.java

詳細に調べ切れていませんが、動画や音声データはcom.android.tv.tunerではあまり触らないようです。ExoPlayerに丸投げですかね?

編集者:すずき(2018/06/29 21:37)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2018年7月2日

libtoolの謎のエラー

Raspberry Pi 3を持っているのですが、あまり速くない(当たり前ですけど)こともあり、ほとんどコンパイルには使っていませんでした。今日、久しぶりにコードのビルドに使ってみたら、変な症状にハマりました。

autoreconfは正常終了?
$ autoconf --version
autoconf (GNU Autoconf) 2.69
...

$ automake --version
automake (GNU automake) 1.15
...

$  autoreconf -fi
aclocal: warning: couldn't open directory 'm4': No such file or directory
configure.ac:23: installing 'conf/compile'
configure.ac:16: installing 'conf/install-sh'
configure.ac:16: installing 'conf/missing'
src/Makefile.am: installing 'conf/depcomp'

更地からのビルドなのでautoreconf -fiを実行しています。この時点では特にエラーも出ずに終わったように見えます。

configureがエラーで落ちる
$ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... /bin/mkdir -p
checking for gawk... gawk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking whether make supports nested variables... (cached) yes
./configure: line 2505: syntax error near unexpected token `ac_ext=c'
./configure: line 2505: `ac_ext=c'

しかしconfigureが謎のエラーで終了してしまいます。しかもconfig.logにエラーの内容が記録されておらず、怪しいです。

しばしconfigure.acをいじってみてわかったことは、以下の条件を満たしていると、このエラーが発生するようです。

  • configure.acにLT_INIT() を記述、つまりlibtoolを使うように記述する
  • システムにlibtoolがインストールされていない
  • 更地(autoconf, automake関連のファイルが全く無い状態)からビルドする

解決策はlibtoolをインストールするか、libtoolを使っていないならconfigure.acからLT_INIT() を削除しても良いです。

この辺の仕組みは詳しくありませんが、libtoolが無いなら無いと言ってくれれば、もう少しわかりやすいのにな…と思います。

編集者:すずき(2018/07/04 23:12)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2018年7月4日

デスクランブラのテスト

先日、作った(2018年6月1日の日記参照)ISDBというかARIBのデスクランブラの続きです。

DVB APIで制御可能なチューナー(私はPT2で確認しています)を使っている方であれば、下記のようにチューニング(コードは GitHubにあります)できます。チューニングに成功して放送が受信されると、/dev/dvb/adapter0/dvr0からスクランブルの掛かったMPEG2-TSが出力されます。

PT2のチューニングのテスト
例: BSプレミアム(衛星はアダプタ0か2、地デジはアダプタ1か3を使います)

$ ./sample_dvb 0 S BS 3 0x4031
...

ごちゃごちゃ出るのが邪魔くさければ、

$ ./sample_dvb 0 S BS 3 0x4031 > /dev/null

スマートカードリーダーをPCに接続し、B-CASカードをリーダーに挿入した上で、下記のようにデスクランブル(コードは GitHubにあります)できます。デスクランブルしたMPEG2-TSはUDPで送るか、ファイルに保存できます。

デスクランブルのテスト
例: 自分自身にUDPで送る

$ ./arib_descramble /dev/dvb/adapter0/dvr0 localhost 1234
...

VLCを起動し、udp://@:1234を再生すると、受信中の放送が映るはずです。

改良

自身の規格理解のためもあって、かなり手抜き実装していて、異常に重いため、いくつか改良してみました。まずプロファイラで見てみると、MULTI2復号と、どこかにある無駄なコピーに、時間がかかっているようです。

MULTI2復号の高速化にはSSE2を使ってみました。MULTI2の復号は8バイトずつですが、SSE2を活用するには32バイトの方が都合が良いです。ですので4単位まとめて(4 x 8バイト = 32バイト = 128bit = SSE2のレジスタ幅)処理して、残った32バイトに満たないデータは従来どおり8バイトずつ処理します。

残念ながら、結果から言うとあまり最適化が効きませんでした。SIMDで高速化できないロード/ストアの割合が高いのか、計算が占める割合が低いのか、いまいちわからなかったのですが、あまり高速化できませんでした。CPU利用率でいうと12% が11% になるか、ならないか…程度です。

無駄なコピーは2箇所見つけたのでガッツリ消しました。これは効果があったようで、CPU利用率でいうと11% が10% くらいまで削減できました。

無駄なコピーはもう1つありましたが、単純に消すわけにいかなくてやや難しそうだったので、また今度にします。

ARMでも実行してみた

PCだと、CPU利用率10% 程度だったので、最近のマルチコアCPUなら割と余裕の負荷です。ではショボいCPUで実行するとどうなるか、試してみました。

手持ちのRaspberry Pi 3(ARM Cortex A53 x 4/1.4GHz)で実行してみたところ、CPU利用率25〜27% 程度でした。動かないかもしれないと思っていたので、正直意外でした。かなり健闘していると思います。

ARMにはNEONというSIMD命令がありますが、NEONを使った復号の高速化にはまだ手を出していません。今度やってみますが、SSE2の結果を見た限りでは、絶大な効果は見込めないでしょう。きっと。

編集者:すずき(2018/07/05 00:44)

コメント一覧

  • すずきさん(2018/07/11 21:26)
    NEON にも対応してみましたが、やはり MULTI2 の復号はボトルネックではないらしく、CPU 負荷はほとんど変わりませんでした……。
open/close この記事にコメントする



2018年7月5日

AndroidとMPEG2-TSその1

その1その2その3その4

Android 8がMPEG2-TSのPSI(Program Specific Information)をどのように処理しているのか、気になったので調べてみました。調査に使ったコードはAOSPのタグandroid-8.1.0_r33です。

PSIのことをセクションと呼ぶ人もいますね。MPEG2 Systemの規格書ISO13818-1/ITU-T H.222.0によれば、PSIはxxx Tableという名前(PATならProgram Association Table)で、テーブルは1つないし、複数のセクション(xxx_sectionという名前で定義されている、PATならprogram_association_section)から構成されるからだと思います。

さておきTSを処理しているところは、下記のようになっています。

Extractor TS受付部分

//frameworks/av/media/libstagefright/mpeg2ts/MPEG2TSExtractor.cpp

status_t MPEG2TSExtractor::feedMore(bool isInit) {
    Mutex::Autolock autoLock(mLock);

    uint8_t packet[kTSPacketSize];
    ssize_t n = mDataSource->readAt(mOffset, packet, kTSPacketSize);

    if (n < (ssize_t)kTSPacketSize) {
        if (n >= 0) {
            mParser->signalEOS(ERROR_END_OF_STREAM);
        }
        return (n < 0) ? (status_t)n : ERROR_END_OF_STREAM;
    }

    ATSParser::SyncEvent event(mOffset);
    mOffset += n;
    status_t err = mParser->feedTSPacket(packet, kTSPacketSize, &event); //★★★★
    if (event.hasReturnedData()) {
        if (isInit) {
            mLastSyncEvent = event;
        } else {
            addSyncPoint_l(event);
        }
    }
    return err;
}

ここで出てくるmParserはATSParserのポインタなので、ATSParserの実装を見てみます。

TS受付部分

//frameworks/av/media/libstagefright/mpeg2ts/ATSParser.cpp

status_t ATSParser::feedTSPacket(const void *data, size_t size,
        SyncEvent *event) {
    if (size != kTSPacketSize) {
        ALOGE("Wrong TS packet size");
        return BAD_VALUE;
    }

    ABitReader br((const uint8_t *)data, kTSPacketSize);
    return parseTS(&br, event); //★★★★
}

status_t ATSParser::parseTS(ABitReader *br, SyncEvent *event) {
    ALOGV("---");
//...
    status_t err = OK;

    unsigned random_access_indicator = 0;
    if (adaptation_field_control == 2 || adaptation_field_control == 3) {
        err = parseAdaptationField(br, PID, &random_access_indicator);
    }
    if (err == OK) {
        if (adaptation_field_control == 1 || adaptation_field_control == 3) {
            err = parsePID(br, PID, continuity_counter,
                    payload_unit_start_indicator,
                    transport_scrambling_control,
                    random_access_indicator,
                    event); //★★★★
        }
    }
//...

status_t ATSParser::parsePID(
        ABitReader *br, unsigned PID,
        unsigned continuity_counter,
        unsigned payload_unit_start_indicator,
        unsigned transport_scrambling_control,
        unsigned random_access_indicator,
        SyncEvent *event) {
    ssize_t sectionIndex = mPSISections.indexOfKey(PID);
//...
    if (sectionIndex >= 0) { //★★★★PATかPMTのPIDならこの条件が成り立つ
        sp<PSISection> section = mPSISections.valueAt(sectionIndex);

ここで出てくるmPSISectionはunsignedをキー、sp<PSISection> を値とするKeyedVectorです。キー0にPATを持っていて、それ以外のキーはPMTのPID(PATが一覧を持っている)です。PMTのPIDはPATを受信したときにATSParser::parseProgramAssociationTable() が追加するようです。

PAT/PMT解析

//frameworks/av/media/libstagefright/mpeg2ts/ATSParser.cpp

status_t ATSParser::parsePID(
        ABitReader *br, unsigned PID,
        unsigned continuity_counter,
        unsigned payload_unit_start_indicator,
        unsigned transport_scrambling_control,
        unsigned random_access_indicator,
        SyncEvent *event) {
    ssize_t sectionIndex = mPSISections.indexOfKey(PID);
//...
    if (sectionIndex >= 0) { //★★★★PATかPMTのPIDならこの条件が成り立つ
        sp<PSISection> section = mPSISections.valueAt(sectionIndex);
//...
        if (PID == 0) {
            parseProgramAssociationTable(&sectionBits); //★★★★PID 0ならPATの解析
        } else {
            bool handled = false;
            for (size_t i = 0; i < mPrograms.size(); ++i) { //★★★★ それ以外はPMTかどうか見る
                status_t err;
                if (!mPrograms.editItemAt(i)->parsePSISection( //★★★★PMTか?
                            PID, &sectionBits, &err)) {
                    continue;
                }
//...

bool ATSParser::Program::parsePSISection(
        unsigned pid, ABitReader *br, status_t *err) {
    *err = OK;

    if (pid != mProgramMapPID) {
        return false;
    }

    *err = parseProgramMap(br); //★★★★PMTだったのでPMTの解析

    return true;
}

status_t ATSParser::Program::parseProgramMap(ABitReader *br) {
    unsigned table_id = br->getBits(8);
    ALOGV("  table_id = %u", table_id);
//...
    // descriptors
    CADescriptor programCA;
    bool hasProgramCA = findCADescriptor(br, program_info_length, &programCA); //★★★★PMTの持っているdescriptorを見ている
    if (hasProgramCA && !mParser->mCasManager->addProgram(
            mProgramNumber, programCA)) { //★★★★CA descriptorの指すPIDつまりECMのPIDを追加
        return ERROR_MALFORMED;
    }
//...
    size_t infoBytesRemaining = section_length - 9 - program_info_length - 4;

    while (infoBytesRemaining >= 5) { //★★★★ エレメンタリストリームのPIDと一緒に付いているdescriptorを見ている
//...
        CADescriptor streamCA;
        bool hasStreamCA = findCADescriptor(br, ES_info_length, &streamCA);
        if (hasStreamCA && !mParser->mCasManager->addStream(
                mProgramNumber, elementaryPID, streamCA)) { //★★★★CA descriptorの指すPIDつまりECMのPIDを追加
            return ERROR_MALFORMED;
        }
//...
    }
//...
    for (size_t i = 0; i < infos.size(); ++i) {
        StreamInfo &info = infos.editItemAt(i);

        if (mParser->mCasManager->isCAPid(info.mPID)) { //★★★★CA descriptorに記載のあったストリーム
            // skip CA streams (EMM/ECM)
            continue;
        }
        ssize_t index = mStreams.indexOfKey(info.mPID);

        if (index < 0) {
            sp<Stream> stream = new Stream(
                    this, info.mPID, info.mType, PCR_PID, info.mCASystemId);

            if (mSampleAesKeyItem != NULL) {
                stream->signalNewSampleAesKey(mSampleAesKeyItem);
            }

            isAddingScrambledStream |= info.mCASystemId >= 0; //★★★★CA descriptorに記載が無いのにスクランブルされている??
            mStreams.add(info.mPID, stream);
        }
    }

ざっくり言うと、スクランブルの掛かったストリームはmParser->mCasManagerに任せ、スクランブルの掛かっていないストリームはmStreamsに任せるようです。

CA descriptorに載っていないのにスクランブルの掛かった変なストリームがあると警告が出るようになっています。

編集者:すずき(2018/07/17 22:45)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2018年7月6日

AndroidとMPEG2-TSその2

その1その2その3その4

昨日の続き。スクランブルの掛かったストリームはmParser->mCasManagerに任せていました。mCasManagerはATSParser::CasManagerでしたので、実装を見てみます。

CasManager

//frameworks/av/media/libstagefright/mpeg2ts/CasManager.cpp

bool ATSParser::CasManager::parsePID(ABitReader *br, unsigned pid) {
    ssize_t index = mCAPidToSessionIdMap.indexOfKey(pid);
    if (index < 0) {
        return false;
    }
    hidl_vec<uint8_t> ecm;
    ecm.setToExternal((uint8_t*)br->data(), br->numBitsLeft() / 8);
    auto returnStatus = mICas->processEcm(mCAPidToSessionIdMap[index], ecm); //★★★★processEcm()
    if (!returnStatus.isOk() || (Status) returnStatus != Status::OK) {
        ALOGE("Failed to process ECM: trans=%s, status=%d",
                returnStatus.description().c_str(), (Status) returnStatus);
    }
    return true; // handled
}

謎のmICasがどこから来るかは、後で調べるとして、関数名processEcm() で探してみると、HALの方にコードがあります。

processEcm() の実装

//hardware/interfaces/cas/1.0/default/CasImpl.cpp

Return<Status> CasImpl::processEcm(
        const HidlCasSessionId &sessionId, const HidlCasData& ecm) {
    ALOGV("%s: sessionId=%s", __FUNCTION__,
            sessionIdToString(sessionId).string());
    std::shared_ptr<CasPlugin> holder = std::atomic_load(&mPluginHolder); //★★★★CasPlugin
    if (holder.get() == nullptr) {
        return toStatus(INVALID_OPERATION);
    }

    return toStatus(holder->processEcm(sessionId, ecm));
}

想像するにCasPluginというクラスを派生させて処理を実装するのでしょう。探してみるとframeworks/av/drm/mediacas/plugins以下にclearkeyとmockという実装があります。

ClearKeyの実装

//frameworks/av/drm/mediacas/plugins/clearkey/ClearKeyCasPlugin.h

class ClearKeyCasPlugin : public CasPlugin {
...

//frameworks/av/drm/mediacas/plugins/clearkey/ClearKeyCasPlugin.cpp

status_t ClearKeyCasPlugin::processEcm(
        const CasSessionId &sessionId, const CasEcm& ecm) {
    ALOGV("processEcm: sessionId=%s", sessionIdToString(sessionId).string());
    sp<ClearKeyCasSession> session =
            ClearKeySessionLibrary::get()->findSession(sessionId);
    if (session == NULL) {
        return ERROR_CAS_SESSION_NOT_OPENED;
    }

    Mutex::Autolock lock(mKeyFetcherLock);

    return session->updateECM(mKeyFetcher.get(), (void*)ecm.data(), ecm.size()); //★★★★mKeyFetcher
}

status_t ClearKeyCasSession::updateECM(
        KeyFetcher *keyFetcher, void *ecm, size_t size) {
//...
    uint64_t asset_id;
    std::vector<KeyFetcher::KeyInfo> keys;
    status_t err = keyFetcher->ObtainKey(mEcmBuffer, &asset_id, &keys); //★★★★keyFetcher
    if (err != OK) {
        ALOGE("updateECM: failed to obtain key (err=%d)", err);
        return err;
    }

    ALOGV("updateECM: %zu key(s) found", keys.size());
    for (size_t keyIndex = 0; keyIndex < keys.size(); keyIndex++) {
        String8 str;

        const sp<ABuffer>& keyBytes = keys[keyIndex].key_bytes;
        CHECK(keyBytes->size() == kUserKeyLength);

        int result = AES_set_decrypt_key(
                reinterpret_cast<const uint8_t*>(keyBytes->data()),
                AES_BLOCK_SIZE * 8, &mKeyInfo[keyIndex].contentKey); //★★★★libsslの関数に渡して鍵を生成している?ようだ
//...


//frameworks/av/drm/mediacas/plugins/clearkey/ClearKeyFetcher.cpp

status_t ClearKeyFetcher::ObtainKey(const sp<ABuffer>& buffer,
        uint64_t* asset_id, std::vector<KeyInfo>* keys) {
//...

引数に渡しているmKeyFetcher(とget() が返すkeyFetcherも同様に)はKeyFetcher型のポインタでした。KeyFetcherを継承したClearKeyFetcher型のオブジェクトが格納されていました。

ClearKeyの仕組みは詳しく知りませんが、ClearKeyCasSession::updateECM() でAESの復号などをしていることと、AESの復号鍵はClearKeyFetcher::ObtainKey() がECMを読んで復号鍵を取得してくれるように見えました。

AndroidでECMの解読を行っている箇所が見つけられました。エレメンタリストリームのデスクランブルはどこで行っているのでしょうね…??

編集者:すずき(2018/07/17 22:45)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2018年7月7日

AndroidとMPEG2-TSその3

その1その2その3その4

デスクランブル処理はどこにあるんでしょう??それらしい処理を辿ってみましたが、本当にこの処理が動くのか、何を切っ掛けにデスクランブル処理が働き始めるのか、動かしてみないとわからなさそうです。うーん……。

デスクランブル処理Java側(たぶん)

//frameworks/base/media/java/android/media/MediaDescrambler.java

    public final int descramble(
            @NonNull ByteBuffer srcBuf, @NonNull ByteBuffer dstBuf,
            @NonNull MediaCodec.CryptoInfo cryptoInfo) {
//...
        try {
            return native_descramble(
                    cryptoInfo.key[0],
                    cryptoInfo.numSubSamples,
                    cryptoInfo.numBytesOfClearData,
                    cryptoInfo.numBytesOfEncryptedData,
                    srcBuf, srcBuf.position(), srcBuf.limit(),
                    dstBuf, dstBuf.position(), dstBuf.limit());
        } catch (ServiceSpecificException e) {
            MediaCasStateException.throwExceptionIfNeeded(e.errorCode, e.getMessage());
        } catch (RemoteException e) {
            cleanupAndRethrowIllegalState();
        }
        return -1;
    }

ここで呼び出しているnative_descramble() はnativeであると宣言されています。つまりJNI経由で呼び出します。メディアフレームワーク(android.media)のJNI実装はframeworks/base/media/jniに配置されているようです。

デスクランブル処理JNI側

//frameworks/base/media/jni/android_media_MediaDescrambler.cpp

static jint android_media_MediaDescrambler_native_descramble(
        JNIEnv *env, jobject thiz, jbyte key, jint numSubSamples,
        jintArray numBytesOfClearDataObj, jintArray numBytesOfEncryptedDataObj,
        jobject srcBuf, jint srcOffset, jint srcLimit,
        jobject dstBuf, jint dstOffset, jint dstLimit) {
    sp<JDescrambler> descrambler = getDescrambler(env, thiz); //★★★★descramblerはJDescrambler型
    if (descrambler == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException",
                "Invalid descrambler object!");
        return -1;
    }

    hidl_vec<SubSample> subSamples;
    ssize_t totalLength = getSubSampleInfo(
            env, numSubSamples, numBytesOfClearDataObj,
            numBytesOfEncryptedDataObj, &subSamples);
    if (totalLength < 0) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                "Invalid subsample info!");
        return -1;
    }
//...
    err = descrambler->descramble(
            key, totalLength, subSamples,
            srcPtr, srcOffset, dstPtr, dstOffset,
            &status, &bytesWritten, &detailedError); //★★★★
//...


status_t JDescrambler::descramble(
        jbyte key,
        ssize_t totalLength,
        const hidl_vec<SubSample>& subSamples,
        const void *srcPtr,
        jint srcOffset,
        void *dstPtr,
        jint dstOffset,
        Status *status,
        uint32_t *bytesWritten,
        hidl_string *detailedError) {
//...
    auto err = mDescrambler->descramble(
            (ScramblingControl) key,
            subSamples,
            mDescramblerSrcBuffer,
            0,
            dstBuffer,
            0,
            [&status, &bytesWritten, &detailedError] (
                    Status _status, uint32_t _bytesWritten,
                    const hidl_string& _detailedError) {
                *status = _status;
                *bytesWritten = _bytesWritten;
                *detailedError = _detailedError;
            }); //★★★★
//...

ここで出てくるmDescramblerはIDescrambler型のポインタです。パッと見では、何がセットされているのか良くわかりませんが、このインタフェースを実装しているのは下記しかなさそうです。

デスクランブラ

//hardware/interfaces/cas/1.0/default/DescramblerImpl.h

class DescramblerImpl : public IDescrambler {
//...

//hardware/interfaces/cas/1.0/default/DescramblerImpl.cpp

Return<void> DescramblerImpl::descramble(
        ScramblingControl scramblingControl,
        const hidl_vec<SubSample>& subSamples,
        const SharedBuffer& srcBuffer,
        uint64_t srcOffset,
        const DestinationBuffer& dstBuffer,
        uint64_t dstOffset,
        descramble_cb _hidl_cb) {
    ALOGV("%s", __FUNCTION__);

    // Get a local copy of the shared_ptr for the plugin. Note that before
    // calling the HIDL callback, this shared_ptr must be manually reset,
    // since the client side could proceed as soon as the callback is called
    // without waiting for this method to go out of scope.
    std::shared_ptr<DescramblerPlugin> holder = std::atomic_load(&mPluginHolder); //★★★★holder = mPluginHolder
    if (holder.get() == nullptr) {
        _hidl_cb(toStatus(INVALID_OPERATION), 0, NULL);
        return Void();
    }

//...

    // Casting hidl SubSample to DescramblerPlugin::SubSample, but need
    // to ensure structs are actually idential

    int32_t result = holder->descramble(
            dstBuffer.type != BufferType::SHARED_MEMORY,
            (DescramblerPlugin::ScramblingControl)scramblingControl,
            subSamples.size(),
            (DescramblerPlugin::SubSample*)subSamples.data(),
            srcPtr,
            srcOffset,
            dstPtr,
            dstOffset,
            NULL);
//...

このmPluginHolderはDescramblerPlugin型です。DescramblerImplが生成される時に設定されます。DescramblerImplを生成するのはMediaCasService::createDescrambler() のみ?のように見えます。

次に出てくるholderにはDescramblerPluginのポインタが入ります。Pluginの実装を探してみるとClearKeyにそれらしき処理があります。

Descrambler pluginの実装

//frameworks/av/drm/mediacas/plugins/clearkey/ClearKeyCasPlugin.cpp

ssize_t ClearKeyDescramblerPlugin::descramble(
        bool secure,
        ScramblingControl scramblingControl,
        size_t numSubSamples,
        const SubSample *subSamples,
        const void *srcPtr,
        int32_t srcOffset,
        void *dstPtr,
        int32_t dstOffset,
        AString *errorDetailMsg) {

    ALOGV("descramble: secure=%d, sctrl=%d, subSamples=%s, "
            "srcPtr=%p, dstPtr=%p, srcOffset=%d, dstOffset=%d",
          (int)secure, (int)scramblingControl,
          subSamplesToString(subSamples, numSubSamples).string(),
          srcPtr, dstPtr, srcOffset, dstOffset);

    if (mCASSession == NULL) {
        ALOGE("Uninitialized CAS session!");
        return ERROR_CAS_DECRYPT_UNIT_NOT_INITIALIZED;
    }

    return mCASSession->decrypt(
            secure, scramblingControl,
            numSubSamples, subSamples,
            (uint8_t*)srcPtr + srcOffset,
            dstPtr == NULL ? NULL : ((uint8_t*)dstPtr + dstOffset),
            errorDetailMsg);
}

ここで出てくるmCASSessionはClearKeyCasSessionを指すようです。setMediaCasSession() にて設定されています。これは後で追ってみます。

ClearKeyによるデスクランブル

//av/drm/mediacas/plugins/clearkey/ClearKeyCasPlugin.cpp

// Decryption of a set of sub-samples
ssize_t ClearKeyCasSession::decrypt(
        bool secure, DescramblerPlugin::ScramblingControl scramblingControl,
        size_t numSubSamples, const DescramblerPlugin::SubSample *subSamples,
        const void *srcPtr, void *dstPtr, AString * /* errorDetailMsg */) {
        return ERROR_CAS_CANNOT_HANDLE;
    

    AES_KEY contentKey;

        // Hold lock to get the key only to avoid contention for decryption
        Mutex::Autolock _lock(mKeyLock);

        int32_t keyIndex = (scramblingControl & 1);
            ALOGE("decrypt: key %d is invalid", keyIndex);
            return ERROR_CAS_DECRYPT;
        
        contentKey = mKeyInfo[keyIndex].contentKey; //★★★★ClearKeyCasSession::updateECM() で設定する鍵だと思う
    
//...
        // Don't decrypt if len < AES_BLOCK_SIZE.
        // The last chunk shorter than AES_BLOCK_SIZE is not encrypted.
        if (scramblingControl != DescramblerPlugin::kScrambling_Unscrambled
                && subSamples[i].mNumBytesOfEncryptedData >= AES_BLOCK_SIZE) {
            err = decryptPayload(
                    contentKey,
                    numBytesinSubSample,
                    subSamples[i].mNumBytesOfClearData,
                    (char *)dst);
        }
//...

// Decryption of a TS payload
status_t ClearKeyCasSession::decryptPayload(
        const AES_KEY& key, size_t length, size_t offset, char* buffer) const {
    CHECK(buffer);

    // Invariant: only call decryptPayload with TS packets with at least 16
    // bytes of payload (AES_BLOCK_SIZE).

    CHECK(length >= offset + AES_BLOCK_SIZE);

    return TpBlockCtsDecrypt(key, length - offset, buffer + offset);
}

// AES-128 CBC-CTS decrypt optimized for Transport Packets. |key| is the AES
// key (odd key or even key), |length| is the data size, and |buffer| is the
// ciphertext to be decrypted in place.
status_t TpBlockCtsDecrypt(const AES_KEY& key, size_t length, char* buffer) {
    CHECK(buffer);
//...

CBCモードでAES暗号を復号しているようです。

まとめると、MediaDescrambler(ドキュメントはここ)のdescrambler() によって、スクランブルされたデータのデスクランブルができそう、ということがわかりました。

ドキュメントにそう書いてあるし、当たり前なんですけどね。新たにデスクランブラを足そうと思う人は、中身も知らないと足せないです。

編集者:すずき(2018/07/17 22:46)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2018年7月8日

AndroidとMPEG2-TSその4

その1その2その3その4

もはや自分以外の誰得の内容なのか、わかりませんが、気にせず書きます。

その3にて「DescramblerImplを生成するのはMediaCasService::createDescrambler() のみ?のように見えます。」と書きました。この関数に至る経路がわかると、デスクランブラがどの暗号系を選択するのか?いつ選択するのか?などがわかるようになります。

で、今回はcreateDescrambler() が呼ばれる経路を見つけたのでメモします。しかしながらExtractorのsetMediaCas() を呼び出すのは誰なのか?まだ謎のままなので、いまいちスッキリしませんけども。

ExtractorのsetMediaCas()

//frameworks/av/media/libstagefright/mpeg2ts/MPEG2TSExtractor.cpp

status_t MPEG2TSExtractor::setMediaCas(const HInterfaceToken &casToken) {
    HalToken halToken;
    halToken.setToExternal((uint8_t*)casToken.data(), casToken.size());
    sp<ICas> cas = ICas::castFrom(retrieveHalInterface(halToken));
    ALOGD("setMediaCas: %p", cas.get());

    status_t err = mParser->setMediaCas(cas); //★★★★mParser
    if (err == OK) {
        ALOGI("All tracks now have descramblers");
        init();
    }
    return err;
}

ここで出てくるmParserの型はATSParserでしたので、ATSParserの実装を見てみます。

ATSParser setMediaCas()

//frameworks/av/media/libstagefright/mpeg2ts/ATSParser.cpp

status_t ATSParser::setMediaCas(const sp<ICas> &cas) {
    status_t err = mCasManager->setMediaCas(cas); //★★★★mCasManager
    if (err != OK) {
        return err;
    }
    for (size_t i = 0; i < mPrograms.size(); ++i) {
        mPrograms.editItemAt(i)->updateCasSessions();
    }
    return OK;
}

ここで出てくるmCasManagerの型はATSParser::CasManagerでした。CasManagerを見てみましょう。

CasManager setMediaCas()

//frameworks/av/media/libstagefright/mpeg2ts/CasManager.cpp

status_t ATSParser::CasManager::setMediaCas(const sp<ICas> &cas) {
    if (cas == NULL) {
        ALOGE("setMediaCas: received NULL object");
        return BAD_VALUE;
    }
    if (mICas != NULL) {
        ALOGW("setMediaCas: already set");
        return ALREADY_EXISTS;
    }
    for (size_t index = 0; index < mProgramCasMap.size(); index++) {
        status_t err;
        if ((err = mProgramCasMap.editValueAt(
                index)->setMediaCas(cas, mCAPidToSessionIdMap)) != OK) { //★★★★mProgramCasMap
            return err;
        }
    }
    mICas = cas; //★★★★mICas
    return OK;
}

Extractorから渡されてきたcasは、CasManagerのメンバ変数mICasに保存されるようです。ちなみにこのmICasはその2のCasManager::parsePID() にて、mICas->processEcm(mCAPidToSessionIdMap[index], ecm); の呼び出し時に出てきました。ここで保存されていたんですね。

話を戻してmProgramCasMapはunsignedがキー、ProgramCasManagerのポインタが値のKeyedVectorです。

ProgramCasManager setMediaCas()

status_t ATSParser::CasManager::ProgramCasManager::setMediaCas(
        const sp<ICas> &cas, PidToSessionMap &sessionMap) {
    if (mHasProgramCas) {
        return initSession(cas, sessionMap, &mProgramCas); //★★★★
    }
    // TODO: share session among streams that has identical CA_descriptors.
    // For now, we open one session for each stream that has CA_descriptor.
    for (size_t index = 0; index < mStreamPidToCasMap.size(); index++) {
        status_t err = initSession(
                cas, sessionMap, &mStreamPidToCasMap.editValueAt(index)); //★★★★
        if (err != OK) {
            return err;
        }
    }
    return OK;
}

status_t ATSParser::CasManager::ProgramCasManager::initSession(
         const sp<ICas>& cas,
         PidToSessionMap &sessionMap,
         CasSession *session) {
    sp<IMediaCasService> casService = IMediaCasService::getService("default"); //★★★★IMediaCasService型
    if (casService == NULL) {
        ALOGE("Cannot obtain IMediaCasService");
        return NO_INIT;
    }
//...
    returnDescrambler = casService->createDescrambler(descriptor.mSystemID); //★★★★createDescrambler()
    if (!returnDescrambler.isOk()) {
        ALOGE("Failed to create descrambler: trans=%s",
                returnDescrambler.description().c_str());
        goto l_fail;
    }
    descramblerBase = (sp<IDescramblerBase>) returnDescrambler;
    if (descramblerBase == NULL) {
        ALOGE("Failed to create descrambler: null ptr");
        goto l_fail;
    }

やっとcreateDescrambler() が出てきました。casServiceはIMediaCasService型ですが、このインタフェースを実装しているクラスは1つしかなさそうです。

MediaCasService setMediaCas()

//hardware/interfaces/cas/1.0/default/MediaCasService.h

class MediaCasService : public IMediaCasService {
//...


//hardware/interfaces/cas/1.0/default/MediaCasService.cpp

Return<sp<IDescramblerBase>> MediaCasService::createDescrambler(int32_t CA_system_id) {

    ALOGV("%s: CA_system_id=%d", __FUNCTION__, CA_system_id);

    sp<IDescrambler> result;

    DescramblerFactory *factory;
    sp<SharedLibrary> library;
    if (mDescramblerLoader.findFactoryForScheme(
            CA_system_id, &library, &factory)) { //★★★★DescramblerPluginを探す処理はこの辺にありそう
        DescramblerPlugin *plugin = NULL;
        if (factory->createPlugin(CA_system_id, &plugin) == OK
                && plugin != NULL) {
            result = new DescramblerImpl(library, plugin); //★★★★DescramblerImplを生成している箇所があった
        }
    }

    return result;
}

ここでゴールのようです。まとめるとExtractorのsetMediaCas() を呼ぶと、

  • ATSParser::CasManager::parsePID() が呼び出しているmICas->processEcm() のmICasが設定される。
    つまりECMの処理方法が決まる
  • DescramblerImpl::mPluginHolderが設定される。
    つまりデスクランブルの処理方法が決まる
編集者:すずき(2018/07/17 22:44)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



link もっと前
2018年6月25日 >>> 2018年7月8日
link もっと後

管理用メニュー

link 記事を新規作成

<2018>
<<<06>>>
-----12
3456789
10111213141516
17181920212223
24252627282930

最近のコメント5件

  • link 24年10月1日
    すずきさん (10/06 03:41)
    「xrdpで十分動作しているので、Wayl...」
  • link 24年10月1日
    hdkさん (10/03 19:05)
    「GNOMEをお使いでしたら今はWayla...」
  • link 24年10月1日
    すずきさん (10/03 10:12)
    「私は逆にVNCサーバーに繋ぐ使い方をした...」
  • link 24年10月1日
    hdkさん (10/03 08:30)
    「おー、面白いですね。xrdpはすでに立ち...」
  • link 14年6月13日
    2048player...さん (09/26 01:04)
    「最後に、この式を出すのに紙4枚(A4)も...」

最近の記事3件

  • link 24年10月28日
    すずき (10/30 23:49)
    「[Linuxからリモートデスクトップ] 目次: Linux開発用のLinuxマシンの画面を見るにはいろいろな手段がありますが、...」
  • link 23年4月10日
    すずき (10/30 23:46)
    「[Linux - まとめリンク] 目次: Linux関係の深いまとめリンク。目次: RISC-V目次: ROCK64/ROCK...」
  • link 24年10月24日
    すずき (10/25 02:35)
    「[ONKYOからM-AUDIOのUSB DACへ] 目次: PCかれこれ10年以上(2013年3月16日の日記参照)活躍してく...」
link もっとみる

こんてんつ

open/close wiki
open/close Linux JM
open/close Java API

過去の日記

open/close 2002年
open/close 2003年
open/close 2004年
open/close 2005年
open/close 2006年
open/close 2007年
open/close 2008年
open/close 2009年
open/close 2010年
open/close 2011年
open/close 2012年
open/close 2013年
open/close 2014年
open/close 2015年
open/close 2016年
open/close 2017年
open/close 2018年
open/close 2019年
open/close 2020年
open/close 2021年
open/close 2022年
open/close 2023年
open/close 2024年
open/close 過去日記について

その他の情報

open/close アクセス統計
open/close サーバ一覧
open/close サイトの情報

合計:  counter total
本日:  counter today

link About www2.katsuster.net
RDFファイル RSS 1.0

最終更新: 10/30 23:49